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

如何测试基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 04:17:22