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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 18:35:01