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

iOS单元测试中Resolver框架引发的间歇性失败问题咨询

问题原因分析
  1. Resolver全局容器共享:Resolver.main是全局单例容器,所有测试用例共用同一个实例。批量执行测试时,不同测试类对DefaultNavigator<Destination>的注册会互相覆盖——后执行的测试注册的Mock会替换先执行测试的Mock,导致先执行的测试的SUT可能注入了不属于自己的Mock实例。
  2. 测试初始化时机问题:你在测试类的init()方法中注册Mock,而XCTest框架批量执行时,可能会提前初始化多个测试类实例,导致容器中的注册信息被提前覆盖,当前测试的SUT拿到的Mock并非自己创建的那个,进而出现popToRootCallsCount状态混乱。
  3. Mock状态未隔离:即使Mock是每个测试类实例单独创建的,但如果容器中的注册被覆盖,SUT实际调用的是其他测试的Mock,其计数会被其他测试的操作修改,导致断言失败。
解决方案:实现测试间的依赖隔离

1. 使用测试生命周期方法(setUp/tearDown)替代init

XCTest的setUp会在每个测试方法执行前调用,tearDown会在每个测试方法执行后调用,利用这两个方法可以确保每次测试都拥有独立的Mock和容器状态:

class DefaultSuccessViewModelTests: XCTestCase {
    private var sut: DefaultSuccessViewModel!
    private var mockNavigator: MockDefaultNavigator<Destination>!

    override func setUp() {
        super.setUp()
        // 重置Resolver容器,清空所有之前的注册
        Resolver.reset()
        // 创建当前测试专属的Mock
        mockNavigator = MockDefaultNavigator<Destination>()
        // 注册当前测试的Mock到容器
        Resolver.register { self.mockNavigator as DefaultNavigator<Destination> }
        // 初始化SUT,确保注入的是当前测试的Mock
        sut = DefaultSuccessViewModel()
    }

    override func tearDown() {
        // 清理实例,避免循环引用和状态残留
        sut = nil
        mockNavigator = nil
        // 再次重置容器,彻底隔离后续测试
        Resolver.reset()
        super.tearDown()
    }

    func test_onUIEvent_onItemTap_popsToRoot() {
        sut.onUIEvent(.onMainTap)
        XCTAssertEqual(mockNavigator.popToRootCallsCount, 1)
    }
}

2. 使用Resolver测试专用容器(进阶)

Resolver提供了测试专用的容器机制,可以避免污染全局容器:

// 在测试文件顶部定义测试容器
let testResolver = Resolver(parent: nil)

class DefaultSuccessViewModelTests: XCTestCase {
    private var sut: DefaultSuccessViewModel!
    private var mockNavigator: MockDefaultNavigator<Destination>!

    override func setUp() {
        super.setUp()
        // 切换Resolver到测试容器
        Resolver.root = testResolver
        testResolver.reset()
        
        mockNavigator = MockDefaultNavigator<Destination>()
        testResolver.register { self.mockNavigator as DefaultNavigator<Destination> }
        sut = DefaultSuccessViewModel()
    }

    override func tearDown() {
        sut = nil
        mockNavigator = nil
        testResolver.reset()
        // 切回全局容器,避免影响其他测试
        Resolver.root = .main
        super.tearDown()
    }

    func test_onUIEvent_onItemTap_popsToRoot() {
        sut.onUIEvent(.onMainTap)
        XCTAssertEqual(mockNavigator.popToRootCallsCount, 1)
    }
}

3. 补充:指定Mock的注册作用域

在测试中,注册Mock时指定.unique作用域,确保每次注入都是全新实例(结合上面的重置方案,可进一步强化隔离):

Resolver.register { self.mockNavigator as DefaultNavigator<Destination> }.scope(.unique)
关键注意点
  • 永远不要在测试类的init()中做依赖注册,因为XCTest的实例初始化时机不受测试执行顺序控制,会导致全局容器状态混乱。
  • 每次测试前后必须重置Resolver容器,彻底清除之前的注册信息,避免测试间的状态泄漏。

内容的提问来源于stack exchange,提问作者Levan Karanadze

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 12:15:55