Design the Test Case

Now we have the tools we need to start testing the network response. Let’s start by adding a test suite ViewControllerTests. Use Test Zero as temporary scaffolding to confirm that you hooked up the test suite. (See Start from Test Zero.) Delete Test Zero once you see its expected failure message.

We want to test how the code handles different network responses. This could include error scenarios, but for now let’s concentrate on the happy path. Think for a moment about how to set up such a test. We want to do the following:

Following the test naming tip from Observe Object Life Cycles to Learn the Phases of a Test, here’s a name that expresses the test:

 test_searchForBookNetworkCall_withSuccessResponse_shouldSaveDataInResults()

Create a test with that name, and follow Load a Storyboard-Based View Controller to load ViewController from the storyboard. Remember that this also means editing Main.storyboard to give the view controller a Storyboard ID. Run the tests, which should pass.

Also follow Make a Test Helper for Button Taps to create the tap(_:) helper. Then add the following to the test. It creates the test spy, injects it, loads the view, and taps the button.

NetworkResponse/NetworkResponseTests/ViewControllerTests.swift
 let​ spyURLSession = ​SpyURLSession​()
 sut.session = spyURLSession
 sut.​loadViewIfNeeded​()
 tap​(sut.button)

We need some test JSON. Let’s do that by defining a multiline string, then converting it to Data. Do this in a helper function so we can use the same data in a couple of tests:

NetworkResponse/NetworkResponseTests/ViewControllerTests.swift
 private​ ​func​ ​jsonData​() -> ​Data​ {
 """
  {
  "​results​": [
  {
  "​artistName​": "​​Artist​​",
  "​trackName​": "​​Track​​",
  "​averageUserRating​": 2.5,
  "​genres​": [
  "​​Foo​​",
  "​​Bar​​"
  ]
  }
  ]
  }
  """​.​data​(using: .utf8)!
 }
images/aside-icons/tip.png

If test JSON is small, define it using a string inside test code. This keeps the input close to the assertions, making their relationship clearer.

When you need to reuse test JSON, create a method in the test class. When you need to parameterize the data, add arguments that the method uses to fill in the JSON details.

When test JSON is large, store it in a file to make it easier to copy and paste from an actual response. Stored JSON files also give you an opportunity to periodically check that they still match real server responses. You can write contract tests[27] that do this work.

To test a successful network response, we should supply an HTTPURLResponse with a status code of 200 for HTTP “OK.” Let’s add a helper method to make a response:

NetworkResponse/NetworkResponseTests/ViewControllerTests.swift
 private​ ​func​ ​response​(statusCode: ​Int​) -> ​HTTPURLResponse​? {
 HTTPURLResponse​(url: ​URL​(string: ​"http://DUMMY"​)!,
  statusCode: statusCode, httpVersion: ​nil​, headerFields: ​nil​)
 }

Now we can call the closure the test spy captured. For the happy path, we supply JSON data, an HTTP response with status code 200, and no error:

NetworkResponse/NetworkResponseTests/ViewControllerTests.swift
 spyURLSession.dataTaskArgsCompletionHandler.​first​?(
 jsonData​(), ​response​(statusCode: 200), ​nil
 )

For our first attempt, let’s assert that the decoded results match the JSON input:

NetworkResponse/NetworkResponseTests/ViewControllerTests.swift
 XCTAssertEqual​(sut.results, [
 SearchResult​(artistName: ​"Artist"​, trackName: ​"Track"​,
  averageUserRating: 2.5, genres: [​"Foo"​, ​"Bar"​])
 ])

This would work if the closure stayed on the same thread. But run tests, and you’ll see a failure message. We need to account for multithreading.