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
财产永远为零。
有任何想法吗?这是单元测试的预期行为吗?
我认为导致单元测试方法中的 为 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)