I've created a custom pushViewController method with a callback once transition completed. It works on the app but can't make it work on unit tests. This is the UINavigationController extension:
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() }
}
}
When I call it inside a unit test transitionCoordinator
is always nil. I've tried setting up a window, setting navigation as root controller, and making that window key and visible. I've also tried accessing view property so VC is loaded but nothing seems to work. transitionCoordinator
property is always nil.
Any ideas? Is this the expected behavior on unit tests?
I think the main problem that causes the transitionCoordinator
to be nil in your unit test method is that the initial viewController(rootViewController) has not yet finished it's appearing transition(this is done by the container). If the transition is not finished then when you call push()
no animation will be performed (and thus no transitionCoordinator
object will be created). Also for some reason unit tests will refuse to make my custom window key, so I used the hosting's app window. I tested the following and it seems to work:
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)
}
I would test your code in a different way. First you should avoid having to wait for animations/transitions to finish. Second you should not test the Apple's code, you should mock all dependencies you have. In your tests you should mock the foreign dependency you have on transitionCoordinator and make it's behavior predictable. You only need to test the behavior of the extension you made, that is that your code will call the coordinator.animate(alongsideTransition: nil)
and execute the completion()
when it's completion handler is called back. Here how to achieve this:
So first you need to refactor your code in order to be able to inject the dependency(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)
}
Then create a mock implementation of the the UIViewControllerTransitionCoordinator
what you will use in the unit tests:
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
}
Then I would write the unit test like this:
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)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With