如何测试基于Combine实现的父子ViewModel间的通信逻辑?
解决父ViewModel通信逻辑的测试方案
针对你遇到的私有PassthroughSubject无法直接测试的问题,这里有几个实用的方案:
1. 测试行为而非内部Subject
父ViewModel订阅Subject后最终会触发特定行为(比如调用Router跳转、更新状态、执行业务逻辑),你可以通过验证这些外部可见的行为来替代直接访问Subject。
比如如果setupBindings里的逻辑是调用Router的导航方法:
navigationSubject .sink { [weak self] value in self?.router.navigate(to: value) } .store(in: &cancellable)
测试时可以给PotatoRouterProtocol创建Mock类,验证navigate(to:)方法是否被正确调用:
class MockPotatoRouter: PotatoRouterProtocol { var didNavigateTo: String? func navigate(to destination: String) { didNavigateTo = destination } } // 测试代码 func testNavigationSubjectTriggersRouter() { let mockRouter = MockPotatoRouter() let mockInteractor = MockInteractor() let vm = PotatoViewModel(router: mockRouter, interactor: mockInteractor) // 通过子ViewModel触发Subject发送事件(需要父ViewModel暴露子VM) vm.childBViewModel.triggerNavigation(value: "test") XCTAssertEqual(mockRouter.didNavigateTo, "test") }
2. 暴露子ViewModel供测试触发事件
如果父ViewModel的Subject是传给子ViewModel的,你可以在PotatoViewModelProtocol中添加只读的子ViewModel属性,测试时通过子ViewModel触发Subject发送事件,然后验证父ViewModel的反应:
// 修改协议 protocol PotatoViewModelProtocol { var childBViewModel: ChildBViewModelProtocol { get } } // 父ViewModel实现 final class PotatoViewModel: PotatoViewModelProtocol { let childBViewModel: ChildBViewModelProtocol // 改为非私有 init(router: PotatoRouterProtocol, interactor: InteractorProtocol) { let navigationSubject = PassthroughSubject<String, Never>() self.childBViewModel = ChildBViewModel(interactor: interactor.getChildBInteractor(), subject: navigationSubject) // ...其他初始化逻辑 } }
测试时直接调用子ViewModel中触发Subject的方法,验证父ViewModel的行为是否符合预期。
3. 测试专属扩展暴露Subject
在测试Target中给PotatoViewModel添加扩展,将私有navigationSubject暴露为测试可用的属性,这样不会影响主Target的封装性:
// 仅在测试文件中添加 extension PotatoViewModel { var testableNavigationSubject: PassthroughSubject<String, Never> { navigationSubject } } // 测试代码 func testNavigationSubjectSink() { let expectation = XCTestExpectation(description: "Subject sends value") let vm = PotatoViewModel(router: MockRouter(), interactor: MockInteractor()) var receivedValue: String? vm.testableNavigationSubject .sink { value in receivedValue = value expectation.fulfill() } .store(in: &cancellables) vm.testableNavigationSubject.send("test") wait(for: [expectation], timeout: 1) XCTAssertEqual(receivedValue, "test") }
这三个方案里,优先推荐第一种(测试行为),因为它更符合黑盒测试的原则,关注ViewModel的对外表现而非内部实现;如果需要更直接的测试,再考虑后两种方案。
内容的提问来源于stack exchange,提问作者André Gonçalves
相关产品推荐
相关产品推荐

