Member preview

How to write better unit tests in Swift — part 2

In part 1 I explain how you can improve the architecture of your app to make it better unit testable.

Writing tests should be as much fun as writing code, right? But why isn’t it? That’s what I asked myself and I noticed that every time I tried to write some tests, I didn’t know where to begin or what to test. And if I had an idea, it turned out I couldn’t test it or I didn’t know how to test it.

After a while I started to realise that if you have a clear structure of your code, you get to know where to put what in your code and it’s becoming clear what to test. In part 1 you can read how to structure your code. In this part you can read what you can test and how you can test it.

In general good tests are flexible but not fragile, which means that if you change one line of code, it should not break lots of tests. Your tests should be extensive, cover all edge cases and run fast. So you can run them often and get feedback as quick as possible.

Structure of a unit test

In a test you can distinguish a subject that you’re going to test, inputs and outputs. It’s good to identify these three different parts because they help you to figure out what you need to do with them. For example you will never stub your test subject, but a dependency you can. You will need as much tests as possible input sets to test their outcomes. And the values of the outputs are verified in the assertions of the test.

The typical structure of a unit test is:

  • Given
  • When
  • Then

In the Given you create mocks and prepare inputs, in the When you execute the method to test on your subject and in the Then you verify the outputs with assertions.

Different types of test doubles

To create real black boxes for our tests we might want to mask some parts of our implementation. We can do this with so-called test doubles from which there are different kinds.

When you must pass an argument in your test, but you know it will never be used in the method being tested, can can pass a dummy. It doesn't matter what the dummy returns.

When you need a dummy that returns a specific value because the rest of the system relies on it to continue running the test, the dummy is called a stub.

If you only want to test if a method is called you can use a spy. A downside is that the more stuff you spy, the tighter you couple your tests with the implementation of your app.

A mock is a spy that has assertions inside of it. It checks what function is called with what arguments, when and how often.

The last flavour we have is a fake. All test doubles discussed so far don’t care about what arguments you pass in. However a fake does, because it has business logic that gives different outputs for different inputs.

Add unit test files to your project

To add a unit test file to your Xcode project you can check unit tests at the start of a new project, or you can add a test target to an existing project later on. Click on your project in the Project Navigator, add a new target and choose a Unit Testing Bundle:

Add unit testing bundle to existing app

At the top of the file you need to import XCTest and make your app target available for testing by adding @testable import YourTarget.

Your class needs to inherit from XCTestCase. There is already a setUp() method to do some initialisation or preparation before a test and a tearDown() method to do some cleanup after a test so tests will not interfere with each other. These methods are called before and after each test. Remember that the unit tests are running randomised, so the order will always change. Also don’t forget to call super in these methods first.

Finally you can write the actual tests. These are written as methods which always need to start with the word test.

Put your test data in one file

To challenge yourself to find the right name for your test-data and prevent doubles, I recommend to put your testdata into one file. I called it Seeds and it can look like this:

struct Seeds {
struct Movies {
static let slumdogMillionaire = Movie(title: “Slumdog
Millionaire”, releaseDate: “2008–11–12”)
        static let hurtLocker = Movie(title: “The Hurt Locker”, 
releaseDate: “2009–01–29”)
        static let kingsSpeech = Movie(title: “The King’s Speech”, 
releaseDate: “noDate”)
        static let all = [slumdogMillionaire, hurtLocker, 
kingsSpeech]
}
}

Test the Interactor

The subject under test (sut) in this test is the ListMoviesInteractor. So the Interactor is the black box that gets it’s inputs from the View Controller.

The test inputs are the methods defined in the ListMoviesBusinessLogic protocol: func fetchMovies()

The test outputs are the methods defined in the ListMoviesPresentationLogic and in the MoviesWorker protocol:
func presentFetchedMovies(_ movies: [Movie]) 
func fetchMovies(completionHandler: @escaping ( [Movie]) -> Void)

We actually need to test if both these outputs are called. What happens inside them we don’t care for now. We will test that when we arrive there. So we can make test doubles of them. To test if both methods are called I replace the presenter and the worker with spies. This substitution lasts only until the end of the test method, so if we would have more tests we would need to set up these test doubles again at the begin of each test.

// MARK: - Test doubles
class ListMoviesPresentationLogicSpy: ListMoviesPresentationLogic {
var presentMoviesCalled = false
var movies: [Movie]?
    func presentFetchedMovies(_ movies: [Movie]) {
presentMoviesCalled = true
self.movies = movies
}
}
class MoviesWorkerSpy: MoviesWorker {
var fetchMoviesCalled = false
var movies: [Movie]

init(movies: [Movie] = []) {
self.movies = movies
}
    override func fetchMovies(completionHandler: @escaping ([Movie]) 
-> Void) {
fetchMoviesCalled = true
completionHandler(movies)
}
}

A test method should test one thing and the name should fully describe what the test is about. So I seperate the tests for calling the presenter and the interactor. In the Given I set up the spy for the worker to check if it’s method is called. Then I initialise the Interactor with the Presenter and the Interactor spy. In When I call the method fetchMovies(), the input of the test, on the subject under test: the interactor. In the Then I do the assert to check if the method in the worker was called (via the spy).

func testFetchMoviesCallsWorkerToFetch() {
    // Given
let moviesWorkerSpy = MoviesWorkerSpy()
let sut = ListMoviesInteractor(presenter:
ListMoviesPresentationLogicSpy(), worker: moviesWorkerSpy)

// When
sut.fetchMovies()
    // Then
XCTAssert(moviesWorkerSpy.fetchMoviesCalled, "fetchMovies()
should ask the worker to fetch movies")
}

Similar is the test to check if the Presenter is called by the Interactor:

func testFetchMoviesCallsPresenterToFormatMovies() {
    // Given
let listMoviesPresentationLogicSpy =
ListMoviesPresentationLogicSpy()
let sut = ListMoviesInteractor(presenter:
listMoviesPresentationLogicSpy, worker: MoviesWorkerSpy())
    // When
sut.fetchMovies()
    // Then
XCTAssert(listMoviesPresentationLogicSpy.presentMoviesCalled,
“fetchMovies() should ask the presenter to format the
movies”)
}

You can also test if the right (amount of) movies are handed over to the presenter:

func testFetchMoviesCallsPresenterToFormatFetchedMovies() {
   // Given
let movies = Seeds.Movies.all
   let listMoviesPresentationLogicSpy = 
ListMoviesPresentationLogicSpy()
let moviesWorkerSpy = MoviesWorkerSpy(movies: movies)
   let sut = ListMoviesInteractor(presenter: 
listMoviesPresentationLogicSpy, worker: moviesWorkerSpy)
   // When
sut.fetchMovies()
   // Then
XCTAssertEqual(listMoviesPresentationLogicSpy.movies?.count,
movies.count, "fetchMovies() should ask the presenter to format
the same amount of movies it fetched")
   XCTAssertEqual(listMoviesPresentationLogicSpy.movies, movies, 
"fetchMovies() should ask the presenter to format the same movies
it fetched")
}

Test the Presenter

The setup of this test class is similar to the interactor but instead the subject under test (sut) is now the ListMoviesPresenter. The input of this test is the presentFetchedMovies(_ movies: [Movie]) method from the ListMoviesPresentationLogic protocol. The outputs are the methods defined in the ListMoviesDisplayLogic protocol, in this case the displayFetchedMovies(_ movies: [ListMoviesViewModel]) method.

// MARK: - Test doubles
class ListMoviesDisplayLogicSpy: ListMoviesDisplayLogic {
var displayFetchedMoviesCalled = false
var displayedMovies: [ListMoviesViewModel] = []

func displayFetchedMovies(_ movies: [ListMoviesViewModel]) {
displayFetchedMoviesCalled = true
displayedMovies = movies
}
}
// MARK: - Tests
func testDisplayFetchedMoviesCalledByPresenter() {
    // Given
let listMoviesDisplayLogicSpy = ListMoviesDisplayLogicSpy()
let sut =
ListMoviesPresenter(viewController:listMoviesDisplayLogicSpy)
    // When
sut.presentFetchedMovies([])
    // Then
XCTAssert(listMoviesDisplayLogicSpy.displayFetchedMoviesCalled,
"presentFetchedMovies() should ask the view controller to
display them")
}

Next to this test we want to test if the formatting of the viewModels from the models is correct. I will do this in a test for the viewModel extension. Next to that we will test when we call the presentFetchedMovies(_ movies: [Movie]) method, we will receive the same amount of viewModels back as models we gave as an argument.

func testPresentFetchedMoviesShouldFormatFetchedMoviesForDisplay() {
    // Given
let listMoviesDisplayLogicSpy = ListMoviesDisplayLogicSpy()
let sut =
ListMoviesPresenter(viewController:listMoviesDisplayLogicSpy)
let movies = Seeds.Movies.all
    // When
sut.presentFetchedMovies(movies)
    // Then
let displayedMovies = listMoviesDisplayLogicSpy.displayedMovies
    XCTAssertEqual(displayedMovies.count, movies.count,
"presentFetchedMovies() should ask the view controller to
display
same amount of movies it receive")
for (index, displayedMovie) in displayedMovies.enumerated() {
XCTAssertEqual(displayedMovie,
ListMoviesViewModel(movies[index]))
}
}

Test the View Controller

Besides participating in the VIP cycle, the View Controller also interacts with the user interface via IBOutlets and IBActions. IBActions are the inputs of the View Controller and IBOutlets are the outputs. Next to that, external things may need to happen when the view loads, appears or disappears. These view lifecycle events are also inputs to the View Controller.

For our View Controller we will check these 3 different kinds of possible inputs we need to test:
1) view lifecycle methods:
 In viewDidLoad() the method fetchMovies() is invoked to make a request to
 the interactor.
2) methods in the ListMoviesDisplayLogic protocol:
 displayFetchedMovies(_ movies: [ListMoviesViewModel]) 
3) IBAction methods: none

The setUp() and tearDown() methods of our tests will be extended with some logic for the view. To make sure the view hierarchy is clean at the beginning of each test we create a new UIWindow object in setUp() and set it to nil in tearDown(). We then set up the sut by instantiating the View Controller. Finally there is a loadView() method that will add the View Controller’s view to the window. The process of bringing the view onto screen does take time and it’s possible the view may not have been fully loaded when your test starts running. When addSubview() is called, the view update events are queued and executed when the run loop gets a chance to run. To solve this timing issue, you can call RunLoop.current.run(until: Date()) to wait with the test for the next tick in the execution cycle. This is important in our case when we test lifecycle events as viewDidLoad().

var window: UIWindow!
// MARK: — Test lifecycle
override func setUp() {
super.setUp()
window = UIWindow()
}
override func tearDown() {
window = nil
super.tearDown()
}
func loadView() {
window.addSubview(sut.view)
RunLoop.current.run(until: Date() )
}

For the first test we call loadView() in Given to get the view hierarchy ready. We then invoke sut.viewDidLoad() in the When. In the Then phase, we assert that fetchMovies() is called by creating the ListMoviesBusinessLogicSpy like we did similarly before for the Interactor and Presenter and check if its variable is set to true.

func testShouldFetchMoviesWhenViewDidLoad() {
    // Given
let listMoviesBusinessLogicSpy = ListMoviesBusinessLogicSpy()
let sut = ListMoviesViewController(interactor:
listMoviesBusinessLogicSpy)
loadView(sut.view)
    // When
sut.viewDidLoad()
    // Then
XCTAssert(listMoviesBusinessLogicSpy.fetchMoviesCalled, "Should
fetch movies when view is loaded")
}

In the second test we want to test if the orders are displayed. This is done by the reloadData() method in Apple’s UITableView. So we need to make sure that this method is called. We can make a spy for that as well by creating a subclass of UITableView.

class TableViewSpy: UITableView {
var reloadDataCalled = false

override func reloadData() {
reloadDataCalled = true
}
}

In the test we set the Table View to the TableViewSpy and call the displayFetchedMovies(_ movies: [ListMoviesViewModel]) method that should trigger the reload.

func testShouldDisplayFetchedMovies() {
    // Given
let tableViewSpy = TableViewSpy()
let sut = ListMoviesViewController()
sut.tableView = tableViewSpy
let viewModels: [ListMoviesViewModel] = []
    // When
sut.displayFetchedMovies(viewModels)
    // Then
XCTAssert(tableViewSpy.reloadDataCalled, “Displaying fetched
movies should reload the table view”)
}

Besides these tests we can write a few tests to check if the number of sections returns 1 and if the number of rows of the Table View is equal to the number of viewModels passed back to the View Controller:

func testNumberOfRowsInAnySectionShouldEqualNumberOfMoviesToDisplay() {
    // Given
let sut = ListMoviesViewController()
let tableView = sut.tableView
let viewModels: [ListMoviesViewModel] =
[ListMoviesViewModel(title: "Test", year: "1988")]
sut.displayFetchedMovies(viewModels)
    // When
let numberOfRows = sut.tableView(tableView!,
numberOfRowsInSection: 1)
    // Then
XCTAssertEqual(numberOfRows, viewModels.count, “The number of
table view rows should equal the number of movies to display”)
}

The last thing we want to test if all the items of the viewModel are right displayed in the right views of the Table View Cell. Because we have one viewModel we only check section 0 and row 0 in this example.

func testShouldConfigureTableViewCellToDisplayOrder() {
    // Given
let sut = ListMoviesViewController()
let tableView = sut.tableView
let viewModels: [ListMoviesViewModel] =
[ListMoviesViewModel(title: “E.T.”, year: “1982”) ]
sut.displayFetchedMovies(viewModels)
    // When
let indexPath = IndexPath(row: 0, section: 0)
let cell = sut.tableView(tableView!, cellForRowAt: indexPath)
as! ListMoviesTableViewCell
    // Then
XCTAssertEqual(cell.titleLabel?.text, “E.T.”, “A properly
configured table view cell should display the movie title”)

XCTAssertEqual(cell.yearLabel?.text, “1982”, “A properly
configured table view cell should display the movie year”)
}

Running the tests

To build your tests in Xcode 9 you can go to Product > Build for > Testing (⇧⌘U) or you can run them and choose Product > Test (⌘U). In the Test Navigator (⌘6) you can see the results of you tests. If you have run the tests once you can see little diamonds before each test in your testfiles. If you only want to re-run one test, you can click on the icon in front of it which will change in a play-icon when you hover over it. Or if you want to re-run all tests of one class you can click the icon in front of the test class. In View > Debug Area > Show Debug Area you can see the test logs. It shows the number of passes and failures and how long it takes to run each test and all the tests.

If you have run the tests, you can go to the Report Navigator (⌘9), click on the last tests and then on Coverage. You will see that except for the initialisation of the View Controller everything is tested for 100%. If you don’t see any coverage go to Product > Schemes > Edit scheme. Then go to Test and the tab Options. If you put a checkmark in front of Gather coverage then you can see the coverage the next time you have run the tests.