You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中Published对象的单元测试问题排查

解决Swift中Published对象单元测试不触发订阅的问题

常见问题点及修复方案

1. 调度器适配测试环境

你的代码使用DispatchQueue.main作为debounce的调度器,但单元测试默认在后台线程运行,主队列的任务可能无法立即执行。可以通过两种方式调整:

  • 将调度器改为可注入的依赖,测试时使用DispatchQueue.immediate或TestScheduler;
  • 在测试中手动刷新主队列,确保闭包执行。

修改ViewModel支持调度器注入:

class MovieListViewModel {
    @Published public private(set) var arrayOfMovies: [Movie] = []
    @Published private var arraofFavoriteMoviesID: [FavouriteMovieID] = []
    private let scheduler: DispatchQueue
    private var subscriptions = Set<AnyCancellable>()

    init(request: NetworkServiceProtocol, scheduler: DispatchQueue = .main) {
        self.scheduler = scheduler
        addSubscribers()
        callServicesInSequence()
    }

    func addSubscribers() {
        $arrayOfMovies.combineLatest($arraofFavoriteMoviesID)
            .debounce(for: 0.0, scheduler: scheduler)
            .sink { [weak self] (_, _) in
                self?.fetchWachedMovies()
                self?.fetchTobeWatchedMovies()
                self?.fetchFavoriteMovies()
            }
            .store(in: &subscriptions)
    }

    // 原有的其他方法...
}

测试时使用即时调度器:

func testPublishedUpdatesTriggerFetch() {
    let mockService = MockNetworkService()
    let viewModel = MovieListViewModel(request: mockService, scheduler: .immediate)
    
    viewModel.arrayOfMovies = [Movie(id: "1")]
    
    // 验证三个fetch方法是否被调用(需给MockService添加标记属性)
    XCTAssertTrue(mockService.fetchWatchedCalled)
    XCTAssertTrue(mockService.fetchToBeWatchedCalled)
    XCTAssertTrue(mockService.fetchFavoriteCalled)
}

2. 保证ViewModel实例生命周期

测试中ViewModel不能提前被释放,如果是局部变量可能在闭包执行前就被销毁。可以将ViewModel设为测试类的属性,或用XCTestExpectation等待异步任务完成:

func testSubscribersTriggerWithExpectation() {
    let expectation = XCTestExpectation(description: "All fetch methods triggered")
    expectation.expectedFulfillmentCount = 3
    
    let mockService = MockNetworkService()
    let viewModel = MovieListViewModel(request: mockService)
    
    // 替换fetch方法实现用于验证
    viewModel.fetchWachedMovies = { expectation.fulfill() }
    viewModel.fetchTobeWatchedMovies = { expectation.fulfill() }
    viewModel.fetchFavoriteMovies = { expectation.fulfill() }
    
    viewModel.arrayOfMovies = [Movie(id: "1")]
    
    wait(for: [expectation], timeout: 1.0)
}

注:需将fetchWachedMovies等方法改为可替换的闭包,或通过协议封装Mock。

3. 修正Private属性的修改方式

arraofFavoriteMoviesID是private属性,测试中无法直接修改,需给ViewModel添加公开方法来更新该属性:

extension MovieListViewModel {
    func addFavoriteMovieID(_ id: FavouriteMovieID) {
        arraofFavoriteMoviesID.append(id)
    }
}

测试时调用该方法触发Published发送:

func testFavoriteIDUpdatesTriggerFetch() {
    let mockService = MockNetworkService()
    let viewModel = MovieListViewModel(request: mockService, scheduler: .immediate)
    
    viewModel.addFavoriteMovieID(FavouriteMovieID(id: "2"))
    
    XCTAssertTrue(mockService.fetchMethodsCalled)
}

4. 移除不必要的Debounce(可选)

如果测试场景不需要防抖逻辑,可以在测试环境中跳过debounce,或直接使用TestScheduler控制时间推进:

func testWithTestScheduler() {
    let scheduler = TestScheduler(initialClock: 0)
    let mockService = MockNetworkService()
    let viewModel = MovieListViewModel(request: mockService)
    
    // 替换addSubscribers中的调度器为TestScheduler
    viewModel.addSubscribers(with: scheduler)
    
    scheduler.schedule(after: scheduler.now) {
        viewModel.arrayOfMovies = [Movie(id: "1")]
    }
    
    scheduler.run()
    
    XCTAssertTrue(mockService.fetchMethodsCalled)
}

核心总结

测试不触发订阅的原因大多是:调度器在测试环境未执行、ViewModel实例提前释放、private属性修改方式错误。按照上述方案调整后,即可正常触发订阅逻辑。

内容的提问来源于stack exchange,提问作者Nupur Rawat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 13:50:27