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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 11:33:16