Mocking API responses

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

typealias QuestionsFetchedCallback = (JSON) -> Void

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

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

struct TriviaAPI: TriviaAPIProviding {
  func loadTriviaQuestions(callback: @escaping QuestionsFetchedCallback) {
    guard let url = URL(string: "http://questions.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 delegates any load call over to its API provider, but you'll update this code soon to make sure that it converts the raw JSON data that the API returns to question models.

Update the viewDidAppear(_:) method of 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. The next step is to create the mock API in the test target so you can use it to provide the question loader with data.

The JSON file in the app target should be removed from the app target and added to the test target. You can leave it in the app folder but make sure to 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:

struct MockTriviaAPI: TriviaAPIProviding {
  func loadTriviaQuestions(callback: @escaping QuestionsFetchedCallback) {

    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. To determine the location of the JSON file, one of the test classes is used to retrieve the current bundle. This is not the absolute best way to retrieve a bundle because it relies on an external factor to exist in the test target. However, structs can't be used to look up the current bundle. Luckily, the compiler will throw an error if the class that is used to determine the bundle is removed so the compiler would quickly error and the mistake can be fixed. After loading the file, the callback is called, and the request has been successfully handled. Now update the test in LoadQuestionsTest, so it used the mock API as follows:

func testLoadQuestions() {
  let mockApi = MockTriviaAPI()
  let questionsLoader = QuestionsLoader(apiProvider: mockApi)
  let questionsLoadedExpectation = expectation(description: "Expected the questions to be loaded")
  questionsLoader.loadQuestions { _ in
    questionsLoadedExpectation.fulfill()
  }

  waitForExpectations(timeout: 5, handler: nil)
}

A lot of apps have way more complex interactions than the one you're testing now. When you get to implementing more complex scenarios, the main ideas about how to architect your app and tests remain the same, regardless of application complexity. Protocols can be used to define a common interface for certain objects. Combining this with dependency-injection like you did for QuestionsLoader helps to isolate the pieces of your code that you're testing, and it enables you to swap out pieces of code to make sure that you don't rely on external factors if you don't have to.

So far, the test suite is not particularly useful. The only thing that's tested at this point is whether QuestionsLoader passes requests on to the TriviaAPIProviding object and whether the callbacks are called as expected. Even though this technically qualifies as a test, it's much better also to test whether the loader object can convert the loaded data into question objects that the app can display.

Testing whether QuestionsLoader can convert JSON into a Question model is a test that's a lot more interesting than just testing whether the callback is called. A refactor such as this might make you wonder whether you should add a new test or modify the existing test.

If you choose to add a new test, your test suite will cover a simple case where you only test that the callback is called and a more complex case that ensures the loader can convert JSON data to models. When you update the existing test, you end up with a test that validates two things. It would make sure that the callback is called but also that the data is converted to models.

While the implications for both choices are similar, the second choice sort off assumes that the callback will be called. You always want to limit your assumptions when writing tests and there's no harm in adding more tests when you add more features. However, if the callback does not get called, none of the tests will work. So in this case, you can work with a single test that makes sure the callback is called and that the loader returns the expected models.

The test you should end up with will have a single expectation and multiple assertions. Writing the test like this makes sure that the expectation for callback is fulfilled when the callback is called, and at the same time you can use assertions to ensure that the data that's passed to callback is valid and correct.

By making QuestionsLoader create instances of a Question model rather than using it to return a dictionary of JSON data, it not only makes the test more interesting, it also improves the app code by making it a lot cleaner.

Right now, the app uses a dictionary of JSON data to display questions. If the JSON changes, you would have to update the view controller's code. If the app grows, you might be using the JSON data in multiple places, making the process of updating quite painful and error-prone. This is why it's a much better idea to use the Codable protocol to convert raw API responses to Question models. Using Codable objects means you can get rid of the JSON dictionaries in the view controllers, which is a vast improvement.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset