Unit Testing in Swift: A Quick Look at Quick
A few days ago, I wrote about how to do dependency injection in Swift using the default XCTest framework. However, I’ve actually been working a lot with Quick to do my unit testing in Swift. As stated on Quick’s Github page:
“Quick is a behavior-driven development framework for Swift and Objective-C. Inspired by RSpec, Specta, and Ginkgo.”
Coming from Rails, the RSpec way of testing is definitely more familiar and readable to me than XCTest.
So today, I want to walk through how to write some ViewController tests with Quick – make sure to read my Unit Testing in Swift: Dependency Injection post for full impact.
The Setup
To get Quick set up, read the instructions on their very well-done README on Github. This process will become even easier once CocoaPods support for Swift is official.
I’m going to use a very basic Minions app as an example. The app just shows a tableView of minion names and images. To get set up, I have a few basic objects set to go:
Minion
The Minion struct holds information about each Minion. To make it simple, each Minion has a unique name, so two Minions with the same name are actually the same Minion (as specified by the Equatable protocol extension):
// Minion.swift struct Minion { let name: String } extension Minion: Equatable {} func == (lhs: Minion, rhs: Minion) -> Bool { return lhs.name == rhs.name }
MinionService
The MinionService would be used to make an API call to get the Minion data in the real world (using AFNetworking or Alamofire). To keep it simple, let’s pretend this is done asynchronously… For this example, it just returns a hard-coded success result (I added an error there in case you want to hard-code it to return an error):
// MinionService.swift import Foundation class MinionService { enum MinionDataResult { case Success([Minion]) case Failure(NSError) } func getTheMinions(completionHandler: (MinionDataResult) -> Void) { println("pretend we're getting minions asynchronously") let minionData = [Minion(name: "Bob"), Minion(name: "Dave")] completionHandler(MinionDataResult.Success(minionData)) // Uncomment if you want to test our an error scenario // let error = NSError(domain: "Error", // code: 400, // userInfo: [NSLocalizedDescriptionKey : "Oops! The Minions are missing on a new fun adventure!"]) // completionHandler(MinionDataResult.Failure(error)) } }
ViewController
Finally, this is the ViewController. Pay special attention to the fetchMinions:
method, since this is what I’ll be testing for the purposes of this post. Notice that fetchMinions:
takes in a minionService as an argument – this is so we can inject our own version of the minionService for testing purposes (you can read more about this in my dependency injection post).
// ViewController.swift import UIKit class ViewController: UITableViewController { var dataSource: [Minion]? override func viewDidLoad() { super.viewDidLoad() fetchMinions() } func fetchMinions(minionService: MinionService = MinionService()) { minionService.getTheMinions { [unowned self](minionDataResult) -> Void in switch (minionDataResult) { case .Success(let minionsData): self.dataSource = minionsData self.tableView.reloadData() case .Failure(let error): let alertController = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .Alert) let okAction = UIAlertAction(title: "Ok", style: .Cancel, handler: nil) alertController.addAction(okAction) self.presentViewController(alertController, animated: true, completion: nil) } } } // MARK: UITableViewDataSource override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataSource?.count ?? 0 } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("minionCell") as UITableViewCell if let minion = dataSource?[indexPath.row] { cell.textLabel?.text = minion.name cell.imageView?.image = UIImage(named: minion.name) } return cell } }
Deciding What to Test
You should test everything of course (your code is only as good as your tests!), but I’ll be focusing on the fetchMinions:
method. I want to test a few things:
- Make sure it actually calls the MinionService (instead of just generating it’s own random list of minions)
- The Success Case: It assigns the array of minions to the data source on success from the MinionService (in your case, you might have other side effects to test)
- The Failure Case: It shows an alert when there is an error from the MinionService.
Quick Test Setup
Now comes the fun part! Create a new ViewControllerSpec.swift file in your test target. To use Quick, you have to import Quick and Nimble into your tests. Also, your test class needs to be a subclass of QuickSpec:
// ViewControllerSpec.swift import UIKit import Quick import Nimble class ViewControllerSpec: QuickSpec { }
Since I’ll be injecting my own fake MinionService, I’m going to set it up right away at the top:
// ViewControllerSpec.swift class ViewControllerSpec: QuickSpec { class Fake_MinionService: MinionService { var getTheMinionsWasCalled = false var fakeResult: MinionService.MinionDataResult? override func getTheMinions(completionHandler: (MinionDataResult) -> Void) { getTheMinionsWasCalled = true completionHandler(fakeResult!) } } }
To start writing Quick tests, you have to override the spec
method:
// ViewControllerSpec.swift class ViewControllerSpec: QuickSpec { // truncated code override func spec() { // THIS IS WHERE THE TESTS GO } }
Now you’re all set up to go!
Writing Quick Tests
If you’ve used XCTest before, you’re probably used to the setup
method. In Quick, you can use beforeEach
instead. It’s a lot more readable – you know this code will run before each test. afterEach
is also available if needed.
In my case, I need to set up my viewController beforeEach test case:
// ViewControllerSpec.swift class ViewControllerSpec: QuickSpec { // truncated code override func spec() { var viewController: ViewController! beforeEach { let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType)) let navigationController = storyboard.instantiateInitialViewController() as UINavigationController viewController = navigationController.topViewController as ViewController UIApplication.sharedApplication().keyWindow!.rootViewController = navigationController let _ = navigationController.view let _ = viewController.view } } }
Now I’m ready to test my fetchMinions
method. I can make that clear by wrapping my tests in a describe block:
// ViewControllerSpec.swift class ViewControllerSpec: QuickSpec { // truncated Fake_MinionService code override func spec() { // truncated beforeEach code describe(".fetchMinions") { // fetchMinions Tests go here } } }
The fetchMinions:
code has two scenarios – one when the API call (via the minionService) succeeds, and one where it fails. We can wrap these two scenarios into a context block:
// ViewControllerSpec.swift class ViewControllerSpec: QuickSpec { // truncated Fake_MinionService code override func spec() { // truncated beforeEach code describe(".fetchMinions") { context("Minions are fetched successfully") { // Success test goes here } context("There is an error fetching minions") { // Failure test goes here } } } }
The outcome of each context should now be wrapped in an it block – there could be multiple it blocks if there are multiple side-effects you need to test.
// ViewControllerSpec.swift class ViewControllerSpec: QuickSpec { // truncated Fake_MinionService code override func spec() { // truncated beforeEach code describe(".fetchMinions") { context("Minions are fetched successfully") { it("sets the minions as the data source") { // Testing happens here! } } context("There is an error fetching minions") { it("shows an alert with error") { // Testing happens here! } } } }
Finally, you can write the code to test your expectation. Quick is set up with multiple expectation matchers to assert expected outcomes – this is where the testing actually happens:
// ViewControllerSpec.swift class ViewControllerSpec: QuickSpec { // truncated Fake_MinionService code override func spec() { // truncated beforeEach code describe(".fetchMinions") { context("Minions are fetched successfully") { it("sets the minions as the data source") { let fakeMinionService = Fake_MinionService() let minions = [Minion(name: "Bob"), Minion(name: "Dave")] fakeMinionService.fakeResult = MinionService.MinionDataResult.Success(minions) viewController.fetchMinions(minionService: fakeMinionService) // first expectation expect(fakeMinionService.getTheMinionsWasCalled).to(beTrue()) // optionals are handled for you! (dataSource in this case!) expect(minions).to(equal(viewController.dataSource)) } } context("There is an error fetching minions") { it("shows an alert with error") { let fakeMinionService = Fake_MinionService() let error = NSError(domain: "Error", code: 400, userInfo: [NSLocalizedDescriptionKey : "Oops! The Minions are missing on a new fun adventure!"]) fakeMinionService.fakeResult = MinionService.MinionDataResult.Failure(error) viewController.fetchMinions(minionService: fakeMinionService) expect(fakeMinionService.getTheMinionsWasCalled).to(beTrue()) // you can use toEventually for asynchronous code // you can also compare type via beAnInstanceOf matcher expect(viewController.presentedViewController).toEventually(beAnInstanceOf(UIAlertController)) } } } }
The Gotchas
I really enjoy writing tests with Quick – they are very human-readable. The one big issue I’ve found with it is XCode argggggg. I haven’t found a way to run my tests individually – I have to re-run the whole test file to re-test one failing test.
And sometimes, when XCode freaks out, I have to run the whole test suite once to get the tests showing up and running correctly – I think it’s because the tests are generated dynamically at runtime.
It’s also very buggy to debug. Sometimes when a test fails, the error message disappears right after appearing. However, I’ve found that if you put in a Test Breakpoint, you can view the message in the Debugger:
Overall, I really like Quick, especially for using with Swift. It deals with the annoying testing with optionals issues, allows assertion of type, makes it very simple to test asynchronous code, and is very organized / human-readable compared to XCTest (which is not ready for Swift yet!).
Have you tried using Quick? What has your experience been? I’d love to hear about it in the comments!
You can view the sourcecode for this post on Github here.