SwiftUI中如何为ObservableObject ViewModel的@Published变量编写单元测试
基于你提供的代码,可行的单元测试实现思路如下
第一步:先修复ViewModel的隐藏bug
你当前的getList方法存在逻辑错误:已经通过services.resolve(ListService.self)拿到了注入的服务实例,却直接调用ListService()新建了实例,导致依赖注入完全失效,测试时无法注入Mock服务。修复后的代码如下:
func getList() { if let listService = self.services.resolve(ListService.self) { // 用注入的listService实例,不要新建 listService.getList() { [weak self] result in switch result { case .success(let dataRecieved): DispatchQueue.main.async { self?.list = dataRecieved } case .failure(let error): print(error) } } } }
第二步:编写Mock ListService用于测试
单元测试需要避免依赖真实网络请求,需要构建Mock服务来模拟返回结果,在测试Target下新增如下实现:
class MockListService: ListService { var mockResult: Result<List, Error>! override func getList(completion: @escaping (Result<List, Error>) -> Void) { completion(mockResult) } }
第三步:编写单元测试用例
需要注意Combine的sink订阅必须持有cancellable,否则订阅会立即释放,无法收到值回调,这也是你当前测试大概率失败的核心原因:
import XCTest import Combine @testable import 你的App模块名 class ListViewModelTest: XCTestCase { // 持有所有订阅,避免提前释放 var cancellables = Set<AnyCancellable>() func testGetListSuccess() { // 1. 准备Mock数据和依赖 let mockServices = Services() let mockList = List(items: [ListItem()]) // 按你的List结构构造测试数据 let mockService = MockListService() mockService.mockResult = .success(mockList) mockServices.register(mockService, for: ListService.self) // 2. 定义测试期望 let actionExpectation = XCTestExpectation(description: "列表数据加载完成") // 3. 初始化ViewModel并订阅属性变化 let listViewModel = ListViewModel(services: mockServices) listViewModel.$list .compactMap { $0 } // 过滤初始nil值,避免第一次空值触发断言 .sink(receiveValue: { list in XCTAssertEqual(list.items.count, 1) actionExpectation.fulfill() }) .store(in: &cancellables) // 4. 等待期望完成 wait(for: [actionExpectation], timeout: 1) } }
可选优化:加速测试执行
如果要跳过DispatchQueue.main.async的等待耗时,可以在测试环境下注入自定义队列替代主队列,或者调用XCTestCase扩展的waitForMainQueue()方法处理主队列待执行任务,不需要设置超时等待。
内容的提问来源于stack exchange,提问作者Mehul Mandalia
相关产品推荐
相关产品推荐

