Mocking API responses

It's common practice to mock API responses when you're testing. In this segment, we're going to implement the mock API that was described before in order to improve the quality and reliability of our test. First, let's define our protocol. Create a new file in the app target and name it TriviaAPIProviding:

typealias QuestionsLoadedCallback = (JSON) -> Void

protocol TriviaAPIProviding {
    func loadTriviaQuestions(callback: @escaping QuestionsLoadedCallback)
}

The protocol only requires a single method right now. If you want to expand this app later, everything related to the Trivia API must be added to the protocol in order to make sure that you can create both an online version for your app and an offline version for your tests. Next, create a file named TriviaAPI and add the following implementation to it:

import Foundation

struct TriviaAPI: TriviaAPIProviding {
    func loadTriviaQuestions(callback: @escaping QuestionsLoadedCallback) {
        guard let url = URL(string: "http://quesions.movietrivia.json")
            else { return }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data,
                let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
                let json = jsonObject as? JSON
                else { return }
            
            callback(json)
        }
    }
}

Lastly, update the QuestionsLoader struct with the following implementation:

struct QuestionsLoader {    
    let apiProvider: TriviaAPIProviding
    
    func loadQuestions(callback: @escaping QuestionsLoadedCallback) {
        apiProvider.loadTriviaQuestions(callback: callback)
    }
}

The question loader now has an apiProvider that it uses to load questions. Currently, it simply delegates any load call over to its API provider, but we'll update this code soon to make sure that we convert the raw JSON data that the API returns to us to question models. First, update our view controller and we'll implement the mock API struct in our test so we can create our first passing test.

Update the viewDidAppear(_:) method of the LoadTriviaViewController as shown in the following code snippet. This implementation uses the loader struct instead of directly loading the data inside the view controller:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    let apiProvider = TriviaAPI()
    let questionsLoader = QuestionsLoader(apiProvider: apiProvider)
    questionsLoader.loadQuestions { [weak self] json in
        self?.triviaJSON = json
        self?.performSegue(withIdentifier: "TriviaLoadedSegue", sender: self)
    }
}

The preceding code is not only more testable, it's also a lot cleaner. Next up, we'll create the mock API inside our test target.

First of all, the JSON file in the app target should be removed from the app target and added to the test target. Drag it into the correct folder and make sure you update the Target Membership so the JSON file is only available in the test target. Now add a new Swift file named MockTriviaAPI to the test target and add the following code to it:

import Foundation
@testable import MovieTrivia

struct MockTriviaAPI: TriviaAPIProviding {
    func loadTriviaQuestions(callback: @escaping QuestionsLoadedCallback) {
        
        guard let filename = Bundle(for: LoadQuestionsTest.self).path(forResource: "TriviaQuestions", ofType: "json"),
            let triviaString = try? String(contentsOfFile: filename),
            let triviaData = triviaString.data(using: .utf8),
            let jsonObject = try? JSONSerialization.jsonObject(with: triviaData, options: []),
            let triviaJSON = jsonObject as? JSON
            else { return }
        
        callback(triviaJSON)
    }
}

This code fetches the locally stored JSON file from the test bundle. In order to determine the exact path to load, we use one of the test classes to retrieve the current bundle. This is not the absolute best way to retrieve a bundle because we rely on an external factor being in our test target. However, we can't use structs to look up the current bundle. Should we remove the class we rely on, the compiler will throw an error and we can easily fix our mistake. Once the file is loaded, we call the callback and we have successfully handled the request.

Mocking APIs is often done with external frameworks. This is mostly because not all APIs are as straightforward as the ones we've just mocked. A lot of apps have way more complex interactions than we've just tested. The main ideas surrounding your testing architecture remain the same, regardless of application complexity. Protocols can help you to define a common interface for certain objects. Combining this with dependency injection like we just did for the QuestionsLoader helps to isolate the pieces of your code that you're testing, and it enables you to switch out pieces of code to make sure that you don't rely on external factors if you don't have to.

So far, our test is not particularly useful. We only test if the QuestionsLoader passes our request on to the TriviaAPIProviding object and if the callbacks are called as expected. Even though this technically qualifies as a test, wouldn't it be better to test whether we can covert the JSON from the API into question objects that our app can display? And when doing so, wouldn't it be nice if we did so by creating instances of a model class instead of JSON?

Testing whether our QuestionsLoader can convert JSON into a Question model is a test that's a lot more interesting than solely testing whether the callback is called. We're now confronted with the question of whether we should add a new test, or modify the existing test. If we create a new test, we're saying that we want to test whether the callback is called and we also want to test whether the loader converted the JSON response to valid models.

If we choose to modify the existing test, we're essentially saying that we assume that the callback will be called. It's been established before that assumptions are bad and we shouldn't make them. But testing if the callback is called and testing whether the callback is called with the correct information could be considered testing the same thing twice. If the callback isn't called, it also won't be called with the correct information.

We'll write a single test with one expectation and multiple assertions. Doing this makes sure that we fulfill our expectation of the callback being called and at the same time we can use assertions to ensure that the data that's passed to the callback is valid and correct. Using the QuestionsLoader to create instances of a model rather than using it like we do now has the added benefit of improving our app's code as well.

Right now, the app uses raw JSON to display questions. If the JSON changes, we are forced to update the view controller. If the app grows, this process becomes more painful because you'd have to search in multiple view controllers. The more manual labor we have to perform in that kind of situation, the bigger the chance that we'll forget or overlook something. This is why it's a much better idea to use the new Codable protocol to encapsulate API responses. Using Codable objects also enables us to get rid of directly accessing JSON in view controllers. It's much cleaner to have direct access to models than having access to raw and dirty JSON.