Introducing the question loader

To make MovieTrivia more testable, you should create a special helper that can load questions. The helper will go to the network and fetch the questions. Once the data is loaded, a callback is called to notify the object that initiated the request about the loaded questions. Because you already know that you're going to write tests for the new helper, you should think of a way to make sure that the helper works with both an offline and an online implementation, so the tests don't have to depend on an internet connection to work.

Because tests should rely on as few outside factors as possible, removing the networking layer from this test would be great. This means that the helper needs to be split into two parts. One part is the helper itself. The other part would be a data-fetcher. The data-fetcher should conform to a protocol that defines the interface that a data-fetcher must have, so you can choose to inject either an online or offline fetcher into the helper.

If the preceding explanation seems a little bit abstract and confusing to you, that's OK. The following code samples will show you the process of separating the different helpers step by step. Add a new Swift file to the application target and call it QuestionsLoader.swift. Then add the following implementation to it:

typealias QuestionsLoadedCallback = (JSON) -> Void

struct QuestionsLoader {
  func loadQuestions(callback: @escaping QuestionsLoadedCallback) {
    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)
    }
  }
}

This struct defines a method to load questions with a callback. This is already nice and a lot more testable than before. You can now isolate the question loader and test it separated from the rest of the app. A test for the helper in its current state would look like the test shown in the following code snippet:

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

  waitForExpectations(timeout: 5, handler: nil)
}

The preceding test creates an instance of QuestionLoader and sets up an expectation. An expectation is used when you expect something to happen in your test eventually. Since QuestionLoader loads its questions asynchronously, you can't expect the questions to be loaded by the time this test method is done executing. The callback that's called when the questions are loaded is used to fulfill the expectation in this test. To make sure that the test waits for the expectation to be fulfilled, waitForExpectations(timeout:handler:) is called after loadQuestions(callback:). If the expectation isn't fulfilled within the five-second timeout that is specified, the test fails.

 

Examine this test closely; you should be able to see all of the As that you read about earlier. The first A, arrange, is where the loader and expectation are created. The second A, act, is when loadQuestions(callback:) is called. The final A, assert, is inside the callback. This test doesn't validate whether the data passed to the callback is valid, but you'll get to that later.

Separating the loader into its own object is great but is still has one problem. There is no way to configure whether it loads data from a local file or the network. In a production environment, the question loader would load data from the network, which would make the test for the question loader depend on the network as well. This isn't ideal because a test that depends on the network might fail for reasons you can't control.

This can be improved by utilizing some protocol-oriented programming and the dependency-injection pattern. This means that you should define a protocol that defines the public API for a networking layer. Then you should implement a networking object in the app target that conforms to the protocol. QuestionsLoader should have a property that holds anything that conforms to the networking protocol. The test target should have its own object that conforms to the networking protocol so you can use that object to provide QuestionsLoader with mock data.

By setting the test up like this, you can take the entire networking logic out of the equation and arrange tests in such a way that the networking doesn't matter. The mock networking layer will respond with valid, reliable responses that can be used as test input.

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

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