UITest批量执行失败但单独运行正常,求异步测试解决方案
问题:UITest批量执行时异步等待断言频繁失败
我正在一个包含大量异步代码的项目中编写UITest,开发阶段单独运行每个测试都能成功,但批量执行时大部分测试都会失败。我已经正确实现了setup和teardown逻辑,排查了很久还是找不到原因,怀疑是等待断言(expectation)的工作机制出了问题。
下面是我每次等待元素出现/消失时都会调用的方法,也是引发多数问题的核心代码:
/* * This method will wait for an element (10 seconds default) afterwards it will assert it existence * * - parameter toAppear:The condition we are waiting for. * - parameter element: The element to be expected * - parameter timeout: The MAX time that the function will wait for the element, 10 seconds if none is given * - parameter file: The file where the error will be displayed, the current file will be used in case none is provided * - parameter line: The code line where the error will be displayed, the current line will be used in case none is provided */ static func assertForElement(toAppear: Bool, _ element: XCUIElement?, timeout: TimeInterval = 10, file: String = #file, line: Int = #line) { guard let currentTestCase = BaseXCTestCase.CurrentTestCase else { return } guard let element = element else { let message = "Element cannot be empty" currentTestCase.recordFailure(withDescription: message, inFile: file, atLine: line, expected: true) return } let existsPredicate = NSPredicate(format: "exists == \(toAppear)") currentTestCase.expectation(for: existsPredicate, evaluatedWith: element, handler: nil) currentTestCase.waitForExpectations(timeout: timeout) { [weak currentTestCase] (error: Error?) in if (error != nil) { let appearMessage = "Failed to find \(String(describing: element)) after \(timeout) seconds." let disappearMessage = "Failed to see \(String(describing: element)) disappear after \(timeout) seconds." currentTestCase?.recordFailure(withDescription: ((toAppear == false) ? disappearMessage : appearMessage), inFile: file, atLine: line, expected: true) } } }
更新:遇到的具体错误提示
- 多数情况下出现:
但此时模拟器中元素明明是可见且可点击的;Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "Expect predicate"exists == 1"for object - 另一种错误:
caught "NSInternalInconsistencyException", "API violation - creating expectations while already in waiting mode."
想问下有没有可靠的UITest异步代码测试方法?
解决方案:修复UITest异步等待的两个核心问题
先针对你遇到的两个错误逐一分析,再给出更可靠的异步等待实现方案:
1. 解决「创建期望时已处于等待模式」的错误
这个错误是因为在waitForExpectations的闭包还在执行时,又调用了assertForElement方法创建新的期望,或者测试用例中存在嵌套的等待逻辑。批量执行时测试用例切换快,很容易触发这个问题。
修复思路:给BaseXCTestCase添加一个状态标记,记录是否正在等待期望,在assertForElement方法开头先检查这个状态:
// 在BaseXCTestCase中添加属性 private var isWaitingForExpectations = false // 修改你的assertForElement方法,开头添加检查 static func assertForElement(toAppear: Bool, _ element: XCUIElement?, timeout: TimeInterval = 10, file: String = #file, line: Int = #line) { guard let currentTestCase = BaseXCTestCase.CurrentTestCase else { return } // 新增:如果正在等待,直接返回或者抛出错误 if currentTestCase.isWaitingForExpectations { let message = "API violation: Cannot create new expectation while already waiting" currentTestCase.recordFailure(withDescription: message, inFile: file, atLine: line, expected: true) return } guard let element = element else { let message = "Element cannot be empty" currentTestCase.recordFailure(withDescription: message, inFile: file, atLine: line, expected: true) return } // 标记开始等待 currentTestCase.isWaitingForExpectations = true let existsPredicate = NSPredicate(format: "exists == \(toAppear)") currentTestCase.expectation(for: existsPredicate, evaluatedWith: element, handler: nil) currentTestCase.waitForExpectations(timeout: timeout) { [weak currentTestCase] (error: Error?) in // 标记等待结束 currentTestCase?.isWaitingForExpectations = false if (error != nil) { let appearMessage = "Failed to find \(String(describing: element)) after \(timeout) seconds." let disappearMessage = "Failed to see \(String(describing: element)) disappear after \(timeout) seconds." currentTestCase?.recordFailure(withDescription: ((toAppear == false) ? disappearMessage : appearMessage), inFile: file, atLine: line, expected: true) } } }
2. 解决「元素可见但等待超时」的问题
这个问题通常是因为XCUIElement的缓存机制:当你获取元素后,它的状态会被缓存,即使界面上元素已经出现,缓存的状态还是不存在,导致断言失败。
修复思路:每次检查前先刷新元素的状态,或者使用XCUIElementQuery重新获取元素,而不是传入已经缓存的XCUIElement实例。
优化后的方法可以改成接受查询而不是元素:
static func assertForElement(toAppear: Bool, query: XCUIElementQuery, timeout: TimeInterval = 10, file: String = #file, line: Int = #line) { guard let currentTestCase = BaseXCTestCase.CurrentTestCase else { return } if currentTestCase.isWaitingForExpectations { let message = "API violation: Cannot create new expectation while already waiting" currentTestCase.recordFailure(withDescription: message, inFile: file, atLine: line, expected: true) return } currentTestCase.isWaitingForExpectations = true // 使用查询的firstMatch,每次都会重新查找元素,避免缓存 let element = query.firstMatch let existsPredicate = NSPredicate(format: "exists == \(toAppear)") currentTestCase.expectation(for: existsPredicate, evaluatedWith: element, handler: nil) currentTestCase.waitForExpectations(timeout: timeout) { [weak currentTestCase] (error: Error?) in currentTestCase?.isWaitingForExpectations = false if let error = error { let elementDescription = query.description let message = toAppear ? "Failed to find \(elementDescription) after \(timeout) seconds. Error: \(error.localizedDescription)" : "Failed to see \(elementDescription) disappear after \(timeout) seconds. Error: \(error.localizedDescription)" currentTestCase?.recordFailure(withDescription: message, inFile: file, atLine: line, expected: true) } } }
调用的时候就传查询,比如:
// 原来的调用:assertForElement(toAppear: true, app.buttons["Login"]) // 改成:assertForElement(toAppear: true, query: app.buttons["Login"])
3. 更可靠的UITest异步测试实践
除了上面的修复,还有几个通用的最佳实践:
- 避免复用XCUIElement实例:每次操作前都通过查询重新获取,防止缓存导致的状态不一致;
- 使用
waitForExistence简化代码:XCTest已经提供了XCUIElement.waitForExistence(timeout:)方法,对于简单的等待元素出现,可以直接用这个方法,不需要自己封装期望;if !app.buttons["Login"].waitForExistence(timeout: 10) { recordFailure(withDescription: "Login button not found", inFile: #file, atLine: #line, expected: true) } - 批量执行时增加测试间的隔离:在teardown中确保app完全退出,比如调用
app.terminate(),避免前一个测试的状态影响下一个测试; - 使用异步等待的超时回调做清理:确保无论等待成功还是失败,都能重置状态,避免影响后续测试;
- 优先使用XCTest原生API:原生API已经处理了很多边缘情况,比如线程安全、状态缓存,自己封装时尽量基于原生API扩展,不要从零开始写。
内容的提问来源于stack exchange,提问作者Tom Kuschka
相关产品推荐
相关产品推荐

