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

协调器单元测试断言失败:视图控制器推入导航栈延迟求解

解决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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 16:25:35