单元测试内的transitionCoordinator nil

Dav*_*ado 7 tdd unit-testing uinavigationcontroller ios

我创建了一个自定义的 PushViewController 方法,在转换完成后会进行回调。它可以在应用程序上运行,但不能使其在单元测试中运行。这是 UINavigationController 扩展:

extension UINavigationController {
  public func pushViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping (Void) -> Void) {
    pushViewController(viewController, animated: animated)
    guard animated, let coordinator = transitionCoordinator else {
      completion()
      return
    }

    coordinator.animate(alongsideTransition: nil) { _ in completion() }

  }
}
Run Code Online (Sandbox Code Playgroud)

当我在单元测试中调用它时transitionCoordinator总是为零。我尝试设置一个窗口,将导航设置为根控制器,并使该窗口键可见。我也尝试过访问视图属性,以便加载 VC,但似乎没有任何效果。transitionCoordinator财产永远为零。

有任何想法吗?这是单元测试的预期行为吗?

Chr*_*nis 2

我认为导致单元测试方法中的 为 nil 的主要问题transitionCoordinator是初始 viewController(rootViewController) 尚未完成它的出现转换(这是由容器完成的)。如果转换未完成,那么当您调用时,push()不会执行任何动画(因此transitionCoordinator不会创建任何对象)。另外,由于某种原因,单元测试将拒绝制作我的自定义窗口键,因此我使用了托管的应用程序窗口。我测试了以下内容,它似乎有效:

func testPushTransition() throws {
    // Use the main app window that it is already key and visible
    // Otherwise it will might not make key your custom window
    let window = UIApplication.shared.keyWindow!

    let viewController = UIViewController()
    let navigationController = UINavigationController(rootViewController: viewController)
    window.rootViewController = navigationController

    let expectation = self.expectation(description: "transition animation completed")

    // Need to wait for the initial view controller to
    // finish the appearance transition
    // In order to the transition to be animated with a coordinator object
    waitToEventually(viewController.viewIfLoaded?.window != nil)

    let vc2 = UIViewController()
    navigationController.pushViewController(vc2, animated: true, completion: {
        expectation.fulfill()
    })

    waitForExpectations(timeout: 3, handler: nil)
}

func waitToEventually(_ test: @autoclosure () -> Bool, timeout: TimeInterval = 1.0, message: String = "") {
    let runLoop = RunLoop.current
    let timeoutDate = Date(timeIntervalSinceNow: timeout)
    repeat {
        if test() {
            return
        }
        runLoop.run(until: Date(timeIntervalSinceNow: 0.01))
    } while Date().compare(timeoutDate) == .orderedAscending
    XCTFail(message)
}
Run Code Online (Sandbox Code Playgroud)

为了更好的测试附注:

我会以不同的方式测试你的代码。首先,您应该避免等待动画/过渡完成。其次,你不应该测试苹果的代码,你应该模拟你拥有的所有依赖项。在你的测试中,你应该模拟你对transitionCoordinator的外部依赖,并使其行为可预测。您只需要测试您所做的扩展的行为,即您的代码将在完成处理程序被回调时调用coordinator.animate(alongsideTransition: nil)并执行。completion()这里如何实现这一点:

因此,首先您需要重构代码以便能够注入依赖项(transitionCoordinator):

public func pushViewController(_ viewController: UIViewController,
                               animated: Bool,
                               completion: @escaping () -> Void,
                               transitionCoordinator: @autoclosure () -> UIViewControllerTransitionCoordinator?) {
    pushViewController(viewController, animated: animated)
    guard animated, let coordinator = transitionCoordinator() else {
        completion()
        return
    }

    coordinator.animate(alongsideTransition: nil) { _ in
        completion()
    }
}

//The old interface can use as default injected property the self.transitionCoordinator
public func pushViewController(_ viewController: UIViewController,
                               animated: Bool,
                               completion: @escaping () -> Void) {
    self.pushViewController(viewController, animated: animated, completion: completion,
                            transitionCoordinator: self.transitionCoordinator)
}
Run Code Online (Sandbox Code Playgroud)

UIViewControllerTransitionCoordinator然后创建您将在单元测试中使用的内容的模拟实现:

class UIViewControllerTransitionCoordinatorMocked: NSObject, UIViewControllerTransitionCoordinator {

var animateWasCalled = false

func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool {

    animateWasCalled = true

    if let completion = completion {
        completion(self)
    }
    return true
}

//... see full example
// https://gist.github.com/csknns/c420818029718f9184887e3be7e6a932

}
Run Code Online (Sandbox Code Playgroud)

然后我会像这样编写单元测试:

func testCustomPushMethod() throws {
    let viewController = UIViewController()
    let navigationController = UINavigationController(rootViewController: viewController)
    let coordinator = UIViewControllerTransitionCoordinatorMocked()

    var completionWasCalled = false
    navigationController.pushViewController(ViewController(),
                                            animated: true,
                                            completion: { completionWasCalled = true}
                                            , transitionCoordinator: coordinator)

    XCTAssertTrue(coordinator.animateWasCalled)
    XCTAssertTrue(completionWasCalled)
}
Run Code Online (Sandbox Code Playgroud)