如何正确测试UIAlertController的呈现?断言失败问题排查
如何正确测试UIAlertController是否被ShopViewController成功呈现?
问题描述
我正在测试一个用于呈现UIAlertController的函数,但测试持续失败。该函数定义如下:
func presentBuyingErrorDiaologue() { let alert = UIAlertController( title: "Warning", message: "Error purchasing item, please retry this action. If that doesn't help, try restarting or reinstalling the app.", preferredStyle: .alert ) let okButton = UIAlertAction(title: "OK", style: .default) alert.addAction(okButton) self.present(alert, animated: true, completion: nil) }
该函数位于ShopViewController类中,我原本的测试逻辑是调用shopViewController.presentBuyingErrorDiaologue()后,使用XCTAssertTrue(shopViewController.presentedViewController is UIAlertController)进行断言,但运行测试时该断言失败。请问正确测试UIAlertController是否为当前呈现视图的方法是什么?
解决方案
你的测试失败主要有两个核心原因:
present(_:animated:completion:)是异步执行的UI操作,你调用完函数立刻断言时,UIKit还没完成弹窗的呈现流程,此时presentedViewController还未更新。- 测试中的
ShopViewController没有被添加到有效的窗口上下文,UIKit无法正常完成present操作。
下面是两种可靠的解决方案:
方案1:利用异步测试+Completion闭包(推荐,可控性更高)
首先稍微修改你的原函数,添加一个可选的completion闭包,这样可以在弹窗完全呈现后触发断言:
func presentBuyingErrorDiaologue(completion: (() -> Void)? = nil) { let alert = UIAlertController( title: "Warning", message: "Error purchasing item, please retry this action. If that doesn't help, try restarting or reinstalling the app.", preferredStyle: .alert ) let okButton = UIAlertAction(title: "OK", style: .default) alert.addAction(okButton) self.present(alert, animated: true, completion: completion) }
然后编写测试代码,使用XCTestExpectation来等待异步操作完成:
func testBuyingErrorDialogueIsPresented() { // 1. 创建ViewController实例并添加到测试窗口 let shopVC = ShopViewController() let testWindow = UIWindow() testWindow.rootViewController = shopVC testWindow.makeKeyAndVisible() // 2. 创建测试期望 let alertPresentedExpectation = expectation(description: "Buying error alert is presented") // 3. 调用弹窗函数,在completion里执行断言 shopVC.presentBuyingErrorDiaologue { // 断言当前呈现的是UIAlertController XCTAssertTrue(shopVC.presentedViewController is UIAlertController) // 可选:额外验证弹窗的内容(标题、按钮等) if let alert = shopVC.presentedViewController as? UIAlertController { XCTAssertEqual(alert.title, "Warning") XCTAssertEqual(alert.actions.first?.title, "OK") XCTAssertEqual(alert.preferredStyle, .alert) } // 满足测试期望 alertPresentedExpectation.fulfill() } // 4. 等待异步操作完成,超时时间设为1秒足够UIKit处理 waitForExpectations(timeout: 1, handler: nil) }
方案2:等待RunLoop处理UI更新(无需修改原函数)
如果你不想修改原函数,可以通过等待主RunLoop完成一次循环,让UIKit有时间处理弹窗的呈现:
func testBuyingErrorDialogueIsPresented() { let shopVC = ShopViewController() let testWindow = UIWindow() testWindow.rootViewController = shopVC testWindow.makeKeyAndVisible() shopVC.presentBuyingErrorDiaologue() // 等待主RunLoop处理完当前的UI更新任务 RunLoop.main.run(until: Date().addingTimeInterval(0.1)) // 此时再执行断言 XCTAssertTrue(shopVC.presentedViewController is UIAlertController) }
这种方式的缺点是依赖固定的等待时间,不同设备上可能有差异,不如方案1可靠。
关键注意点
- 测试中必须将
ShopViewController添加到UIWindow并设为可见,否则UIKit没有上下文来呈现弹窗,presentedViewController始终为nil。 - 优先选择方案1,通过completion闭包来触发断言,这种方式是完全同步于弹窗呈现完成的时机,不会出现超时或提前断言的问题。
内容的提问来源于stack exchange,提问作者Erika
相关产品推荐
相关产品推荐

