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:

  1. Make sure it actually calls the MinionService (instead of just generating it’s own random list of minions)
  2. 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)
  3. 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:

Quick Test Message

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.

Enjoy the article? Join over 14,500+ Swift developers and enthusiasts who get my weekly updates.

  • Awesome post! Sorry for any frustrations with Quick… a few notes:

    > 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.

    As of Quick v0.2.2, you can “focus” examples. Use fit instead of it to only run a single example. See https://github.com/Quick/Quick#temporarily-running-a-subset-of-focused-examples for details. Also, running an individual test via the test navigator works for me, but apparently not for everyone–we’re tracking that issue here: https://github.com/Quick/Quick/issues/221.

    > Sometimes when a test fails, the error message disappears right after appearing.

    I believe this is a manifestation of https://github.com/Quick/Quick/issues/157. Clearing dervied data always fixes the issue for me, but we are working on identifying the root cause and fixing it/filing a radar.

    > 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.

    Yes, unforunately this is the case–the issue is tracked here: https://github.com/Quick/Quick/issues/219. We’re working on filing a radar or perhaps even hacking Xcode/XCTest to fix this.

    Let me know if you have any other issues or feature requests–feel free to open an issue on GitHub!

    • Thanks Brian for the Quick tips!

      • gmensah

        Hey Natasha, what are your thoughts on using Quick vs Fox?

  • Neeraj Kumar

    Why not used XCTest specially when they have introduced asynchronous testing as well?

    • Ivan Schuetz

      It has a different syntax… and apparently XCTest doesn’t work very well with Swift… at least this was the case in 2014. Maybe this has improved now. Here’s an overview, maybe it helps. http://www.mokacoding.com/blog/ios-testing-in-2015/

  • Ivan Schuetz

    One disadvantage of Quick compared to XCTest I just noticed (I just started with Quick – I may be wrong) is that it seems to not be possible to do nested async calls/tests. And println doesn’t seem to work…