DispatchQueue.main.async相关测试不稳定问题的优化咨询
断言主线程执行的测试不稳定问题
本地重复运行测试均能通过,但CI环境中偶尔会出现「超出10秒超时,未满足预期」的错误。以下是简化后的代码:
生产代码
final class TestClass { var testProtocol: TestProtocol init(testProtocol: TestProtocol) { self.testProtocol = testProtocol } func funcToTest() { performUIUpdate { testProtocol.doSomething() } } } func performUIUpdate(using closure: @escaping () -> Void) { if Thread.isMainThread { closure() } else { DispatchQueue.main.async(execute: closure) } } protocol TestProtocol { func doSomething() }
测试代码
final class TestProtocolMock: TestProtocol { var doSomethingClosure: (() -> Void)? func doSomething() { doSomethingClosure?() } } func test_funcToTest_thenDoSomethingExecuteOnMainQueue() { // 不稳定的测试 let testProtocol = TestProtocolMock() testProtocol.doSomethingClosure = { // 断言 XCTAssertTrue(Thread.isMainThread) expectation.fulfill() // =========>> 超时错误 } let sut = TestClass(testProtocol: testProtocol) // 执行测试操作 DispatchQueue.global(qos: .background).async { sut.funcToTest() } wait(for: [expectation], timeout: 10) }
增加超时时间能降低错误出现频率,但不确定不稳定是否源于DispatchQueue.main.async。除了模拟主队列,还有哪些方法能让测试更稳定?
更新
即使改用主线程触发测试的写法,依然存在不稳定问题:
生产代码更新后:
func funcToTest() { Task() { aService.request() { performUIUpdate { testProtocol.doSomething() } } } }
测试代码:
func test_funcToTest_thenDoSomethingExecuteOnMainQueue() { // 不稳定的测试 testProtocol.doSomethingClosure = { // 断言 XCTAssertTrue(Thread.isMainThread) expectation.fulfill() // =========>> 超时错误 } let sut = TestClass(testProtocol: testProtocol) // 执行测试操作 sut.funcToTest() wait(for: [expectation], timeout: 10) }
解决方案
1. 强制排空主队列任务
CI环境主队列可能因资源紧张延迟调度,导致expectation无法及时触发。在wait前添加代码强制主队列任务执行:
// 等待前触发主队列RunLoop,排空积压任务 RunLoop.main.run(until: Date()) // 或用sync阻塞等待主队列空闲 DispatchQueue.main.sync {}
2. 提升测试用队列优先级
DispatchQueue.global(qos: .background)优先级极低,CI环境中可能被系统节流。测试时改用更高优先级队列:
DispatchQueue.global(qos: .userInitiated).async { sut.funcToTest() }
3. 避免预期多次触发
给XCTestExpectation设置assertForOverFulfill = true,提前发现重复触发问题,同时确保fulfill仅被调用一次:
let expectation = XCTestExpectation(description: "doSomething executed on main thread") expectation.assertForOverFulfill = true
4. 单独测试线程切换逻辑
直接测试performUIUpdate的线程切换行为,减少对业务逻辑的依赖:
func test_performUIUpdate_switchesToMainThreadWhenOffMain() { let expectation = XCTestExpectation(description: "closure runs on main") DispatchQueue.global().async { performUIUpdate { XCTAssertTrue(Thread.isMainThread) expectation.fulfill() } } wait(for: [expectation], timeout: 2) } func test_performUIUpdate_runsImmediatelyOnMainThread() { var executed = false performUIUpdate { XCTAssertTrue(Thread.isMainThread) executed = true } XCTAssertTrue(executed) }
5. 优化Task异步场景
使用Task时,默认在后台线程执行,测试时可显式指定主Actor,或等待前触发RunLoop:
// 强制Task在主线程执行 Task { @MainActor in sut.funcToTest() } // 等待后小幅度触发RunLoop,确保异步任务完成 wait(for: [expectation], timeout: 10) RunLoop.main.run(until: Date().addingTimeInterval(0.1))
6. 减少线程断言的直接依赖
修改Mock记录调用线程状态,用轮询检查替代闭包内的即时断言,降低对调度时机的敏感度:
// 修改TestProtocolMock final class TestProtocolMock: TestProtocol { var didCallDoSomethingOnMainThread = false func doSomething() { didCallDoSomethingOnMainThread = Thread.isMainThread } } // 测试代码 func test_funcToTest_thenDoSomethingExecuteOnMainQueue() { let testProtocol = TestProtocolMock() let sut = TestClass(testProtocol: testProtocol) DispatchQueue.global(qos: .userInitiated).async { sut.funcToTest() } let expectation = XCTestExpectation(description: "doSomething called") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { XCTAssertTrue(testProtocol.didCallDoSomethingOnMainThread) expectation.fulfill() } wait(for: [expectation], timeout: 2) }
内容的提问来源于stack exchange,提问作者phuong vb
相关产品推荐
相关产品推荐

