Testing view controllers with Quick and Nimble
Marcio Klepacz iOS Engineering @ GetYourGuide - Swift Berlin 2015
Overview
• The Problem
• Tools
• Example
• Conclusion
The Problem
View Controllers
• The place where the user interface connects with the app logic and model
Controller
Model View
• One of the pillars of the architecture.
• Sensible code.
• Involuntary change can damage.
Testing View Controllers
• Not easy
• lifecycle managed by the framework.
• lot’s of states.
😰
Tools
Behaviour Driven Development (BDD)
BDD BDD+View Controllers
Tests that verify what an application does are
behavioural tests.
What a view controllers does rather than how.
Check the behaviour not pieces of code.
Robust testing.
More readable. More readable tests.
Quick and Nimble
• Quick is a Behaviour-driven development framework for Swift and Objective-C.
• Inspired by RSpec,Specta, and Ginkgo.
• Nimble is a Matcher Framework also for both languages.
• Provide more clear expectations.
Example
iOS App: Pony
• PonyTabController: UITabBarController.
• Responsible for presenting the app intro
PonyTabController
public class PonyTabController: UITabBarController { override public func viewDidAppear(animated: Bool) { //… if !appIntroHasBeenPresented {
presentViewController(appIntroViewController,…) { appIntroViewController.dismissButtonTapHandler = {
appIntroHasBeenPresented = true self.dismissViewControllerAnimated(true,…) }
• Check if app intro has been presented ⚠ • Present app intro. ⚠ • Dismiss if handler is called. ⚠
Testing: Present app intro ⚠import Quick import Nimble
class PonyTabBarControllerSpec: QuickSpec { override func spec() { describe(“.viewDidAppear"){ context("when app intro had never been dismissed"){ it("should be presented”){
expect(tabBarController.presentedViewController) .toEventually(beAnInstanceOf(AppIntroViewController))
} } } //…
• Pre-conditions are still missing. ⚠
• The object under test is not being invoked. ⚠
Testing: Present app intro ⚠import Pony //… var tabBarController: PonyTabController! describe(".viewDidAppear"){
context("when app intro had never been dismissed"){
beforeEach { // 1 Arrange: tabBarController = storyboard.instantiateInitialViewController() as! PonyTabController
// 2 Act: let _ = tabBarController.view
} it("should be presented”){
// 3 Assert: expect(tabBarController.presentedViewController).toEventually(beAnInstanceOf(AppIntro…))
} }
} } 1. Pre-conditions. ✅
2. The object under test is being invoked. ✅
3. Asserting. ⚠
"Arrange-Act-Assert"
• Pattern for arranging and formatting code in Tests methods.
• Benefit:
• Clearly separates what is being tested from the setup and verification steps.
Testing: Present app intro ⚠import Pony //… var tabBarController: PonyTabController! describe(".viewDidAppear"){
context("when app intro had never been dismissed"){
beforeEach { // 1 Arrange: tabBarController = storyboard.instantiateInitialViewController() as! PonyTabController
// 2 Act: let _ = tabBarController.view
} it("should be presented”){
// 3 Assert: expect(tabBarController.presentedViewController).toEventually(beAnInstanceOf(AppIntro…))
} }
} } 1. Pre-conditions. ✅
2. The object under test is being invoked. ✅
3. Asserting. ⚠
Testing: Present app intro ✅
PonyTabController: UITabBarController { override public func viewDidAppear(animated: Bool) { //… presentViewController(appIntroViewController,…) {
} //…
Warning: Attempt to present <AppIntroViewController: 0x1e56e0a0> on <PonyTabController: 0x1ec3e000>
whose view is not in the window hierarchy!
Testing: Present app intro ✅import Pony //… var tabBarController: PonyTabController! describe(".viewDidAppear"){
context("when app intro had never been dismissed"){
beforeEach { // 1 Arrange: tabBarController = storyboard.instantiateInitialViewController() as! PonyTabController
// 2 Act: UIApplication.sharedApplication().keyWindow?.rootViewController = tabBarController
it("should be presented”){
// 3 Assert: expect(tabBarController.presentedViewController).toEventually(beAnInstanceOf(AppIntro…))
} }
} } 1. Pre-conditions. ✅
2. The object under test is being invoked. ✅
3. Asserting. ✅
Testing: Dismiss app intro ⚠//… context("when app intro had never been dismissed”){ //… context("and dismiss button was tapped") { beforeEach { // Arrange: appIntroHasBeenPresented = false
// Act: tabBarController.beginAppearanceTransition(true, animated: false) tabBarController.endAppearanceTransition()
var appIntroViewController = tabBarController.presentedViewController as! AppIntroViewController appIntroViewController.dismissButton! .sendActionsForControlEvents(UIControlEvents.TouchUpInside)
} it("should dismiss app intro"){ // Assert: expect(tabBarController.presentedViewController).toEventually(beNil())
} //…
}• Another context.
• beginAppearanceTransition: will trigger viewWillAppear.
• endAppearanceTransition: will trigger viewDidAppear.
• sendActionsForControlEvents: simulate tap on the dismiss button
Testing: Dismiss app intro ✅//… context("when app intro had never been dismissed”){
//… context("and dismiss button was tapped") {
beforeEach { // Arrange: appIntroHasBeenPresented = false
// Act: tabBarController.beginAppearanceTransition(true, animated: false) tabBarController.endAppearanceTransition()
var appIntroViewController = tabBarController.presentedViewController as! AppIntroViewController appIntroViewController.dismissButton!
.sendActionsForControlEvents(UIControlEvents.TouchUpInside) }
it("should set appIntroHasBeenPresented to true""){
// Assert: expect(appIntroHasBeenPresented).to(beTrue())
}
it("should dismiss app intro"){ // Assert: expect(tabBarController.presentedViewController).toEventually(beNil())
} //…
}
• Set app intro presented to be true. ✅
• Will dismiss if the button tap handler is called. ✅
Extra//…
waitUntil { done in tabBarController.dismissViewControllerAnimated(false) { done()
} }
}
//…
waitUntil { done in NSThread.sleepForTimeInterval(0.5) done()
} //…
• waitUntil is a function provided by Nimble where you can execute something inside it closure and call done() when is ready.
• Useful when waiting for a callback.
Tested ✅
• Verifying the app intro will be presented if it had never been dismissed. ✅
• Set app intro presented to be true. ✅
• Will dismiss if the button tap handler is called. ✅
//… it("should be presented”){ expect(tabBarController.presentedViewController).toEventually(beAnInstanceOf(AppIntro…))
} //… it("should dismiss app intro"){ expect(tabBarController.presentedViewController).toEventually(beNil())
}
it("should set appIntroHasBeenPresented to true"){ expect(appIntroHasBeenPresented).to(beTrue()) } //…
Conclusion
Conclusion• BDD + Quick and Nimble can help you get more
meaningful tests for view controllers.
• There is a lifecycle that must be followed, i.e: you can’t present a V.C. if there another already being presented or the view is not part of the hierarchy.
• UIKit provide public methods that can help.
• Don’t create massive view controllers.
Questions ?
References
• https://github.com/Quick/Quick • http://realm.io/news/testing-in-swift/ • http://www.slideshare.net/bgesiak/everything-you-never-
wanted • http://c2.com/cgi/wiki?ArrangeActAssert
Top Related