协调器单元测试断言失败:视图控制器推入导航栈延迟求解
解决Coordinator导航单元测试的断言时机问题
针对你遇到的导航操作异步完成导致断言失败的问题,这里有几个无需使用DispatchQueue.main.asyncAfter()的可靠解决方案:
方案1:利用RunLoop同步处理主队列任务
UIKit的导航操作(如push)会在主队列调度执行,有时测试代码的断言会先于导航栈更新完成。通过让主RunLoop执行一次循环,可以确保所有pending的主队列任务(包括导航栈更新)都处理完毕后再执行断言。
func test_pushToBViewController() { // 触发Coordinator的导航操作 coordinator.navigateToB() // 让主RunLoop处理完当前所有待执行任务 RunLoop.main.run(until: Date()) XCTAssert(coordinator.navigationController.topViewController is BViewController) }
这种方式轻量简洁,不需要额外的代理或Mock,适合简单的测试场景。
方案2:监听导航控制器didShow事件,结合XCTestExpectation
通过监听导航控制器的didShow代理回调,在视图控制器真正完成显示时再标记测试期望完成,完全贴合导航的实际生命周期,不会出现提前断言的问题。
func test_pushToBViewController() { let pushExpectation = XCTestExpectation(description: "BViewController已完成推入") // 设置导航控制器代理,用于监听导航完成事件 coordinator.navigationController.delegate = self self.currentExpectation = pushExpectation // 执行导航操作 coordinator.navigateToB() // 等待期望完成,设置合理超时时间(1秒足够覆盖绝大多数导航场景) wait(for: [pushExpectation], timeout: 1.0) XCTAssert(coordinator.navigationController.topViewController is BViewController) } // 让测试类实现UINavigationControllerDelegate extension YourTestClass: UINavigationControllerDelegate { private var currentExpectation: XCTestExpectation? { get { objc_getAssociatedObject(self, &AssociatedKeys.expectation) as? XCTestExpectation } set { objc_setAssociatedObject(self, &AssociatedKeys.expectation, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { if viewController is BViewController { currentExpectation?.fulfill() // 重置代理,避免影响其他测试用例 navigationController.delegate = nil } } } private enum AssociatedKeys { static var expectation = "expectation" }
注:这里使用关联对象存储期望,避免在测试类中添加额外的存储属性导致测试用例间的干扰。
方案3:Mock导航控制器,同步验证操作
如果你的Coordinator设计时依赖抽象的导航协议而非具体的UINavigationController,可以通过Mock导航器来完全规避UIKit的异步逻辑,直接验证Coordinator的行为是否符合预期。
第一步:定义导航协议
protocol NavigationHandler: AnyObject { func pushViewController(_ viewController: UIViewController, animated: Bool) var topViewController: UIViewController? { get } } // 让UINavigationController遵守协议,不影响生产代码使用 extension UINavigationController: NavigationHandler {}
第二步:修改Coordinator依赖协议
class YourCoordinator { weak var navigationHandler: NavigationHandler? init(navigationHandler: NavigationHandler) { self.navigationHandler = navigationHandler } func navigateToB() { let bVC = BViewController() navigationHandler?.pushViewController(bVC, animated: true) } }
第三步:编写测试用例
class MockNavigationHandler: NavigationHandler { private(set) var pushedViewControllers: [UIViewController] = [] var topViewController: UIViewController? { pushedViewControllers.last } func pushViewController(_ viewController: UIViewController, animated: Bool) { pushedViewControllers.append(viewController) } } func test_pushToBViewController() { let mockNavigator = MockNavigationHandler() let coordinator = YourCoordinator(navigationHandler: mockNavigator) coordinator.navigateToB() XCTAssertTrue(mockNavigator.topViewController is BViewController) }
这种方案的优势是测试完全同步,执行速度快,且能精准验证Coordinator是否正确触发了导航操作,适合需要大量单元测试的场景。
内容的提问来源于stack exchange,提问作者Juan Manuel Gentili
相关产品推荐
相关产品推荐

