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:
Instantiate the view controller from the storyboard.
Create an instance of SpyURLSession.
Inject the SpyURLSession into the view controller. Make sure to do this before loading the view to avoid having any methods use the real URLSession.
Simulate the button tap to start the network call. This will call the test spy, which will capture the arguments—including the closure.
Call the captured closure with any arguments we want for testing. For the happy path, this will include JSON data and a response with the “OK” status code. We can test that the data was decoded into the results property.
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.
| 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:
| private func jsonData() -> Data { |
| """ |
| { |
| "results": [ |
| { |
| "artistName": "Artist", |
| "trackName": "Track", |
| "averageUserRating": 2.5, |
| "genres": [ |
| "Foo", |
| "Bar" |
| ] |
| } |
| ] |
| } |
| """.data(using: .utf8)! |
| } |
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:
| 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:
| spyURLSession.dataTaskArgsCompletionHandler.first?( |
| jsonData(), response(statusCode: 200), nil |
| ) |
For our first attempt, let’s assert that the decoded results match the JSON input:
| 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.