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
相关产品推荐
相关产品推荐

