XCTWaiter.wait异步场景非预期超时 单元测试失败问题排查
问题描述
出于测试目的,我编写了如下测试函数:
func test_wait() { var string: String? DispatchQueue.main.async { string = "set" print("string set") } let notNilPredicate = NSPredicate(format: "self != nil") let notNilExpectation = expectation(for: notNilPredicate, evaluatedWith: string) print("start waiting") let waitResult = XCTWaiter.wait(for: [notNilExpectation], timeout: 5) XCTAssert(waitResult == .completed, "wait for notNilExpectation failed with result \(waitResult)") }
该测试运行失败。
等待返回结果为.timedOut,运行日志如下:
start waiting string set … : XCTAssertTrue failed - wait for notNilExpectation failed with result XCTWaiterResult(rawValue: 2) Interrupting test
我无法理解为何var string已经被赋值,等待操作仍然失败。
但如果注释掉DispatchQueue.main.async,即同步执行对应代码块时,测试可以成功通过,此时运行日志为:
string set start waiting Test Case '-[ShopEasyTests.CoreDataCloudKitContainerTest test_wait]' passed (1.087 seconds).
按照我的理解,异步版本的测试逻辑应该也能正常运行,请问该写法存在什么问题?
问题根因
这个写法的错误本质是对expectation(for:evaluatedWith:)的机制和Swift值类型特性理解有偏差:
String?是Swift值类型,调用expectation(for:evaluatedWith:)时,会把当前string的值(初始状态为nil)做一次拷贝传入,expectation全程持有的是这份独立的拷贝值。后续你在异步闭包里修改的是原作用域下的string变量,expectation持有的拷贝不会发生任何变化,自然永远满足不了self != nil的谓词判定。- 同步场景能通过完全是时序巧合:去掉
DispatchQueue.main.async后,string = "set"的执行时机早于expectation创建,传入的拷贝值本身就是非nil的,谓词直接判定成立,不需要等待。
日志中打印的string set只能证明原变量确实被赋值了,但expectation根本观察不到这个变化,超时是必然结果。
修正方案
异步测试优先使用fulfill()主动标记操作完成的标准写法,不要用谓词监听值类型变量:
func test_wait() { var string: String? let exp = expectation(description: "string is set after async operation") DispatchQueue.main.async { string = "set" print("string set") exp.fulfill() // 异步逻辑执行完成后主动触发期望达成 } print("start waiting") waitForExpectations(timeout: 5) XCTAssertNotNil(string) }
如果确实需要用谓词监听的方式,必须保证被监听对象是支持KVO的引用类型,比如将字符串封装到继承自NSObject的类中,用@objc dynamic标记可观察属性:
class ObservableBox: NSObject { @objc dynamic var string: String? } func test_wait() { let box = ObservableBox() DispatchQueue.main.async { box.string = "set" print("string set") } let notNilPredicate = NSPredicate(format: "string != nil") let notNilExpectation = expectation(for: notNilPredicate, evaluatedWith: box) print("start waiting") let waitResult = XCTWaiter.wait(for: [notNilExpectation], timeout: 5) XCTAssertEqual(waitResult, .completed) }
内容的提问来源于stack exchange,提问作者Reinhard Männer
相关产品推荐
相关产品推荐

