Using Quick and Nimble for testing in iOS

interface-builder-teams-header.png

Learn how to use the Quick and Nimble frameworks for testing iOS projects. Follow a small demo application to implement some example tests.

Introduction

Introduction

Unit testing helps us to have confidence that our code is behaving the way we expect it to. It helps us to identify issues before we publish our code. Quick is a testing framework and Nimble is a matching framework. They are easy to use and have the advantage that they are very descriptive of what you are testing when compared with XCTest. We’re going to look at how to get started with Quick and Nimble and write a few simple tests.

Prerequisites

We’re going to be building upon the “Using MVVM in iOS” tutorial that I wrote. Use this project as your starter project. Remember when using the starter project to change the development team to your own within the general settings.

Main Content

Installation

Open terminal & move into your working directory. Initialize your pods project by:

1pod init

Open the newly created Podfile within your favourite text editor. Edit your Podfile so that the WeatherForecastTests target now looks like this:

1target 'WeatherForecastTests' do
2      inherit! :search_paths
3      pod 'Quick'
4      pod 'Nimble'
5    end

Save the file and return your terminal and your working directory. Run the command:

1pod install

This will install your new frameworks and create a pods project. It will also generate a workspace. You should now use the new workspace when working on your project. So if you’ve opened your project already close it and in open the WeatherForecast.xcworkspace instead.

Setting up your test class

Within your tests target create a new group and file by:

  • Highlighting WeatherForcastTests.
  • FileNewGroup.
  • Rename the new group ModelTests.
  • Highlight your new group.
  • FileNewFile.
  • Select Swift file, press Next.
  • Name the new file CurrentWeatherSpecs.
  • Press Create.

Within your new file replace the contents with the following:

1import Quick
2    import Nimble
3    @testable import WeatherForecast
4
5    class CurrentWeatherSpecs: QuickSpec {
6
7    }

This imports the Quick and Nimble frameworks and also imports the main target for testing.

Creating stub data

Create a new group and file by:

  • Highlighting WeatherForcastTests.
  • FileNewGroup.
  • Rename the new group StubData.
  • Highlight your new group.
  • FileNewFile.
  • Select Empty file, press Next.
  • Name the new file london_weather_correct.json.
  • Press Create.

Copy and paste in the contents of the londonWeather.json file.

Writing your first test

Within your CurrentWeatherSpecs.swift file add the following code:

1override func spec() {
2      var sut: CurrentWeather!
3      //1 
4      describe("The 'Current Weather'") {
5        //2
6        context("Can be created with valid JSON") {
7          //3
8          afterEach {
9            sut = nil
10          }
11          //4
12          beforeEach {
13            //5
14            if let path = Bundle(for: type(of: self)
15            ).path(forResource:"london_weather_correct",
16                              ofType: "json") {
17                  do {
18                    let data = try Data(contentsOf: URL(fileURLWithPath: path),
19                                        options: .alwaysMapped)
20                    let decoder = JSONDecoder()
21                    decoder.keyDecodingStrategy = .convertFromSnakeCase
22                    sut = try decoder.decode(CurrentWeather.self, from: data)
23                  } catch {
24                    fail("Problem parsing JSON")
25                  }
26                }
27            }
28            //6
29            it("can parse the correct lat") {
30              //7
31              expect(sut.coord.lat).to(equal(1))
32            }
33            it("can parse the correct date time") {
34              expect(sut.dt).to(equal(0))
35            }
36          }
37        }
38    }
  • Describe the class or structure you are testing.
  • context – under what context is the class or structure being tested. In this example we’re supplying valid JSON. You could pass in invalid JSON and ensure it handles this correctly as well.
  • afterEach – similar to tear down, it is run after each of the tests within this context/scope.
  • beforeEach – similar to setup, it is run before each of the tests within this context/scope.
  • Here we pull in the JSON file from our stub data. This is the data that we will be testing against within this context.
  • it – here we describe what we about to test. It should be a clear statement of what we are testing and we should be able to test it with just a single expectation.
  • expect – this is the “actual“ test, we expect sut.coord.lat to equal 1. Quick & Nimble makes that quite clear by just reading the line of code.

If you run these tests (CMD+U) they will fail. Make sure you have selected a simulator or device to run the tests on. You will receive messages such:

expected to equal <1>, got <51.51>

You will also notice in the test navigator (in the left side panel) a useful test name with a failed test symbol next to it:

The_CurrentWeather__Can_be_created_with_valid_JSON__can_parse_the_correct_lat()

This is the reason why I like testing with Quick and Nimble, it’s extremely easy to see what you have tested and what exactly has failed. You may go ahead and fix these tests by supplying the got values from the error messages into the expect statements.

Testing your ViewController

First lets, create a new group in our tests folder called ViewControllers. Within this folder create a new Swift file called ViewControllerSpecs.swift. Finally open up your Main.storyboard select the ViewController and within the identity inspector (third from left) set the storyboard identifier to ViewController.

Replace your ViewControllerSpecs.swift code with snippet below. You will notice that a lot of this is similar to that of testing the model.

1import Quick
2    import Nimble
3    @testable import WeatherForecast
4
5    class ViewControllerSpecs: QuickSpec {
6        override func spec() {
7            var sut: ViewController!
8            describe("The 'View Controller'") {
9                context("Can show the correct labels text") {
10                    afterEach {
11                        sut = nil
12                    }
13                    beforeEach {
14                        // 1
15                        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
16                        sut = storyboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
17                        _ = sut.view
18                        if let path = Bundle(for: type(of: self)
19                            ).path(forResource: "london_weather_correct", ofType: "json") {
20                            do {
21                                let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
22                                let decoder = JSONDecoder()
23                                decoder.keyDecodingStrategy = .convertFromSnakeCase
24                                sut.searchResult = try decoder.decode(CurrentWeather.self, from: data)
25                            } catch {
26                                fail("Problem parsing JSON")
27                            }
28                        }
29                    }
30                    it("can show the correct text within the coord label") {
31                        // 2
32                        expect(sut.coordLabel.text).to(equal("Lat: 51.51, Lon: -0.13"))
33                    }
34                    it("can show the correct location label") {
35                        expect(sut.locationLabel.text).to(equal("Location: London"))
36                    }
37                }
38            }
39        }
40    }
  • Here we load the view and view controller from the storyboard. This makes sure our UIElements are not nil.
  • Notice how we can also compare strings within the expect statement.

If you run these tests you will notice again that they fail. You’ll notice that the label text is still returning our default Label text. This is because the labels are updated on the main thread. We can change the expectation use toEventually instead.

1expect(sut.coordLabel.text).toEventually(equal("Lat: 51.51, Lon: -0.13"))

Your tests should now pass.

Conclusion

The finished example project can be found here.

We’ve learnt how to write some simple tests for your model and view controller using Quick and Nimble. There are a lot more commands that you may found useful when working within your own project, head to the GitHub repo to learn more.