In this episode, we use the MockClient class to unit test the FeedViewModel class. The unit tests for the FeedViewModel class will change as the project evolves. The primary goal of this episode is to show how the MockClient class helps us write unit tests for the FeedViewModel class.

Laying the Foundation

We first need to add a target for the unit tests. Choose New > Target... from Xcode's File menu and select the Unit Testing Bundle template from the iOS > Test section.

Adding a Unit Testing Bundle

Set Product Name to CocoacastsTests and click Finish at the bottom to create the target.

Adding a Unit Testing Bundle

Remove CocoacastsTests.swift. We start from scratch. We start by adding a few groups and files to the CocoacastsTests group. The first group we add is named Mock API. This group contains the mock data for the unit tests. We add a file with name episodes_success.json to the Mock API group. As the name implies, episodes_success.json contains a list of episodes.

We add another group to the CocoacastsTests group and name it Cases. This group contains the unit tests for the Cocoacasts target. We add a separate group for the unit tests of the Feed tab and name it Feed.

The next step is adding a Swift file to the Feed group by choosing the Unit Test Case Class template from the iOS > Source section. Name the Swift file FeedViewModelTests.swift. If Xcode offers to configure an Objective-C bridging header for you, then click Don't Create.

Before we write any unit tests, we add an import statement for the Cocoacasts module and prefix it with the testable attribute.

import XCTest
@testable import Cocoacasts

class FeedViewModelTests: XCTestCase {

    ...

}

Creating the API Client

In this episode, we focus on unit testing the happy path. Unit testing other scenarios is similar, only the mock data and the assertions differ. The first unit test we write tests execution of the episodesDidChange handler. We want to make sure the handler is executed when the array of Episode objects the FeedViewModel class manages changes.

As I mentioned earlier, fetching the list of episodes in the initializer isn't something I recommend. Remember that we currently fetch the list of episodes in the initializer to populate the Feed tab. This is a temporary solution. Later in this series, we refine the implementation and only fetch the list of episodes at the appropriate time.

We create a method in the FeedViewModelTests class and name it testEpisodesDidChange(). We need to create a FeedViewModel instance and for that we need an object that conforms to the APIClient protocol. That's where the MockClient class comes into play.

import XCTest
@testable import Cocoacasts

class FeedViewModelTests: XCTestCase {

    // MARK: - Set Up & Tear Down

    override func setUp() {}

    override func tearDown() {}

    // MARK: - Tests for Episodes Did Change

    func testEpisodesDidChange() {

    }

}

We define a private, lazy, variable property, apiClient, of type APIClient. Remember that the MockClient class conforms to the APIClient protocol. We use a self-executing closure to create and configure the MockClient instance. We first create the MockClient instance. The next step is adding a response for the episodes endpoint. We need to obtain the URL for episodes_success.json in the unit testing bundle.

import XCTest
@testable import Cocoacasts

class FeedViewModelTests: XCTestCase {

    // MARK: - Properties

    private lazy var apiClient: APIClient = {
        // Initialize Mock Client
        let mockClient = MockClient()

        return mockClient
    }()

    // MARK: - Set Up & Tear Down

    override func setUp() {}

    override func tearDown() {}

    // MARK: - Tests for Episodes Did Change

    func testEpisodesDidChange() {

    }

}

We create a helper method to obtain the URL for the mock data. Create a group in the CocoacastsTests group and name it Extensions. Add a Swift file with name XCTestCase+Helpers.swift to the Extensions group. Add an import statement for the XCTest framework and create an extension for the XCTestCase class.

import XCTest

extension XCTestCase {

}

We define a method with name urlForMockData(with:success:). The first argument is of type String and represents the name of the mock data. The second argument is of type Bool and indicates whether the request is successful or not. We want to have the flexibility to unit test the unhappy path.

import XCTest

extension XCTestCase {

    private func urlForMockData(with name: String, success: Bool) -> URL {

    }

}

We start by creating the file name of the mock data. The strategy is simple for now. The file name consists of the value of the name parameter, an underscore, and the suffix success or failure, depending on the value of the success parameter.

import XCTest

extension XCTestCase {

    func urlForMockData(with name: String, success: Bool) -> URL {
        // Create File Name
        let fileName = "\(name)_\(success ? "success" : "failure")"
    }

}

We then load the unit testing bundle by invoking the init(for:) initializer of the Bundle class, passing in the type of the XCTestCase subclass. To obtain the URL for the mock data, we invoke url(forResource:withExtension:) on the Bundle instance, passing in the file name as the first argument and json as the second argument. Notice that we forced unwrap the result of url(forResource:withExtension:). If url(forResource:withExtension:) returns nil, then we made a mistake we need to fix.

import XCTest

extension XCTestCase {

    func urlForMockData(with name: String, success: Bool) -> URL {
        // Create File Name
        let fileName = "\(name)_\(success ? "success" : "failure")"

        // Fetch URL for Mock Data
        return Bundle(for: type(of: self)).url(forResource: fileName, withExtension: "json")!
    }

}

Let's return to the FeedViewModelTests class and complete the implementation of the apiClient property. We need to invoke the add(response:for:) method on the MockClient instance to associate the mock data with the episodes endpoint. To future proof the API, I would like to add a computed property to the APIEndpoint enum.

Add another Swift file to the CocoacastsTests > Extensions group and name it APIEndpoint+Helpers.swift. Add an import statement for the Cocoacasts module and prefix the import statement with the testable attribute. Create an extension for the APIEndpoint enum.

import Foundation
@testable import Cocoacasts

extension APIEndpoint {

}

Define a computed property, fileName, of type String. We ask the APIEndpoint for its path and replace every forward slash with an underscore. That's it.

import Foundation
@testable import Cocoacasts

extension APIEndpoint {

    var fileName: String {
        return path.replacingOccurrences(of: "/", with: "_")
    }

}

Everything is in place to finish the implementation of the apiClient property in FeedViewModelTests.swift. The first argument we pass to the add(response:for:) method is a Response object. For the success case, we invoke the urlForMockData(with:success:) method, passing in the value of the fileName property we implemented a moment ago. We pass true for the success parameter. We pass episodes as the second argument of the add(response:for:) method.

// MARK: - Properties

private lazy var apiClient: APIClient = {
    // Initialize Mock Client
    let mockClient = MockClient()

    // Configure Mock Client
    mockClient.add(response: .success(urlForMockData(with: APIEndpoint.episodes.fileName, success: true)), for: .episodes)

    return mockClient
}()

Unit Testing the Episodes Did Change Handler

With the apiClient property in place, we can continue implementing the testEpisodesDidChange() unit test. We create a FeedViewModel instance, passing the APIClient instance to the initializer.

// MARK: - Tests for Episodes Did Change

func testEpisodesDidChange() {
    // Create View Model
    let viewModel = FeedViewModel(apiClient: apiClient)
}

Remember that the mock data is fetched asynchronously. This means we need to write a unit test that can handle asynchronous execution. Expectations are designed for this purpose. We ask the XCTestCase instance for an expectation by invoking the expectation(description:) method.

// MARK: - Tests for Episodes Did Change

func testEpisodesDidChange() {
    // Create View Model
    let viewModel = FeedViewModel(apiClient: apiClient)

    // Create Expectation
    let expectation = self.expectation(description: "Episodes Did Change")
}

The unit test fails if the expectation isn't fulfilled or if the expectation isn't fulfilled in time. We install the episodesDidChange handler and fulfill the expectation in the handler. This simply means that the expectation is only fulfilled if the episodesDidChange handler is executed.

// MARK: - Tests for Episodes Did Change

func testEpisodesDidChange() {
    // Create View Model
    let viewModel = FeedViewModel(apiClient: apiClient)

    // Create Expectation
    let expectation = self.expectation(description: "Episodes Did Change")

    // Install Handler
    viewModel.episodesDidChange = {
        // Fulfill Expectation
        expectation.fulfill()
    }
}

The next step is asking the XCTWaiter class to wait for the expectation to be fulfilled. The first argument is an array of expectations. The second argument is a timeout. If the array of expectations are not fulfilled within one second, the unit test fails. We store the result of the wait(for:timeout:) method in a constant with name result.

// MARK: - Tests for Episodes Did Change

func testEpisodesDidChange() {
    // Create View Model
    let viewModel = FeedViewModel(apiClient: apiClient)

    // Create Expectation
    let expectation = self.expectation(description: "Episodes Did Change")

    // Install Handler
    viewModel.episodesDidChange = {
        // Fulfill Expectation
        expectation.fulfill()
    }

    // Wait for Expectations
    let result = XCTWaiter.wait(for: [ expectation ], timeout: 1.0)
}

If result isn't equal to completed, we explicitly fail the unit test by invoking the XCTFail() function.

// MARK: - Tests for Episodes Did Change

func testEpisodesDidChange() {
    // Create View Model
    let viewModel = FeedViewModel(apiClient: apiClient)

    // Create Expectation
    let expectation = self.expectation(description: "Episodes Did Change")

    // Install Handler
    viewModel.episodesDidChange = {
        // Fulfill Expectation
        expectation.fulfill()
    }

    // Wait for Expectations
    let result = XCTWaiter.wait(for: [ expectation ], timeout: 1.0)

    if result != .completed {
        XCTFail("Test Did Time Out")
    }
}

Let's give it a try. Click the diamond on the left of the FeedViewModelTests class definition to run the unit test. The unit test should pass without issues.

Running a Unit Test in Xcode

Unit Testing Number of Episodes

The next unit test we write focuses on the numberOfEpisodes computed property of the FeedViewModel class. The unit test is similar in some respects. Create a unit test with name testNumberOfEpisodes().

We create a FeedViewModel instance and an expectation. We fulfill the expectation in the episodesDidChange handler and use the XCTWaiter class to wait for the expectation to be fulfilled.

// MARK: - Tests for Number of Episodes

func testNumberOfEpisodes() {
    // Create View Model
    let viewModel = FeedViewModel(apiClient: apiClient)

    // Create Expectation
    let expectation = self.expectation(description: "Episodes Did Change")

    // Install Handler
    viewModel.episodesDidChange = {
        // Fulfill Expectation
        expectation.fulfill()
    }

    // Wait for Expectations
    let result = XCTWaiter.wait(for: [ expectation ], timeout: 1.0)
}

To test the numberOfEpisodes computed property, we need to add two assertions. The first assertion verifies that the value of numberOfEpisodes is equal to 0 before the list of episodes is fetched.

// MARK: - Tests for Number of Episodes

func testNumberOfEpisodes() {
    // Create View Model
    let viewModel = FeedViewModel(apiClient: apiClient)

    // Create Expectation
    let expectation = self.expectation(description: "Episodes Did Change")

    // Install Handler
    viewModel.episodesDidChange = {
        // Fulfill Expectation
        expectation.fulfill()
    }

    // Assertions
    XCTAssertEqual(viewModel.numberOfEpisodes, 0)

    // Wait for Expectations
    let result = XCTWaiter.wait(for: [ expectation ], timeout: 1.0)
}

The value of numberOfEpisodes should be equal to 20, the number of episodes in episodes_success.json, after the episodesDidChange handler is executed. This is straightforward to unit test. We use a switch statement to switch on result and add a case for completed and a default case. In the body of the completed case, we assert that numberOfEpisodes is equal to 20. In the body of the default case, we invoke the XCTFail() function. The body of the default case should only be executed if the expectation isn't fulfilled in time.

// MARK: - Tests for Number of Episodes

func testNumberOfEpisodes() {
    // Create View Model
    let viewModel = FeedViewModel(apiClient: apiClient)

    // Create Expectation
    let expectation = self.expectation(description: "Episodes Did Change")

    // Install Handler
    viewModel.episodesDidChange = {
        // Fulfill Expectation
        expectation.fulfill()
    }

    // Assertions
    XCTAssertEqual(viewModel.numberOfEpisodes, 0)

    // Wait for Expectations
    let result = XCTWaiter.wait(for: [ expectation ], timeout: 1.0)

    switch result {
    case .completed:
        // Assertions
        XCTAssertEqual(viewModel.numberOfEpisodes, 20)
    default:
        XCTFail("Test Did Time Out")
    }
}

Unit Testing Presentable for Index

Unit testing the presentable(for:) method of the FeedViewModel class is similar. Create a unit test with name testPresentableForIndex() and copy the body of the testNumberOfEpisodes method as a starting point. Remove the first assertion. We won't be needing it for this unit test.

In the body of the completed case of the switch statement, we ask the view model for an EpisodePresentable object by invoking the presentable(for:) method, passing in 1 as the index. Because we control the list of episodes returned by the MockClient instance, we know exactly what we can expect. We add a handful of assertions to make sure the data the EpisodePresentable object returns matches what we expect.

// MARK: - Tests for Presentable for Index

func testPresentableForIndex() {
    // Create View Model
    let viewModel = FeedViewModel(apiClient: apiClient)

    // Create Expectation
    let expectation = self.expectation(description: "Episodes Did Change")

    // Install Handler
    viewModel.episodesDidChange = {
        // Fulfill Expectation
        expectation.fulfill()
    }

    // Wait for Expectations
    let result = XCTWaiter.wait(for: [ expectation ], timeout: 1.0)

    switch result {
    case .completed:
        // Fetch Presentable
        let presentable = viewModel.presentable(for: 1)

        // Assertions
        XCTAssertEqual(presentable.title, "Debugging User Interface Issues")
        XCTAssertEqual(presentable.author, "by Jim Johnson")
        XCTAssertEqual(presentable.category, "Xcode")
        XCTAssertEqual(presentable.collection, "Debugging Applications With Xcode")
        XCTAssertEqual(presentable.formattedDuration, "07:06")
        XCTAssertEqual(presentable.publishedAt, "Apr 10, 2018")
    default:
        XCTFail("Test Did Time Out")
    }
}

Run the test suite by choosing Product > Test from Xcode's menu or pressing Command + U. The unit tests of the FeedViewModel class should be fast and pass without issues.

Running the Test Suite In Xcode

What's Next?

By using the MockClient class, we have complete control over the unit tests for the FeedViewModel class. This results in a robust and fast test suite. Both attributes are key for a reliable test suite.