Building a Weather Application From Scratch

Mocking Network Requests With Mockingjay

In the previous episodes, we used dependency injection to easily replace objects with mock objects when the unit tests are executed. It's a solution that works fine and it can be applied in a wide range of scenarios.

There's another popular option that is aimed specifically at mocking network requests. A number of libraries allow you to control the response of the network requests an application performs. The most popular options are OHHTTPStubs and Mockingjay.

OHHTTPStubs is written in Objective-C and it's been around for many years. Mockingjay is written in Swift and, like OHHTTPStubs, ships with a wide range of features.

In this episode, I show you how to mock network requests using Mockingjay. It's easy to integrate and use in Swift projects.

Installing Mockingjay

Integrating Mockingjay in a project is straightforward with CocoaPods. Open Terminal, navigate to the root of the project, and execute the pod init command to set up CocoaPods.

pod init

The pod init command creates a file with name Podfile at the root of the project. Open the Podfile in a text editor of your choice to edit it.

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'Rainstorm' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for Rainstorm

  target 'RainstormTests' do
    inherit! :search_paths
    # Pods for testing
  end

end

I don't discuss CocoaPods in detail in this episode. The Podfile defines the dependencies of the project. Remove the comments and set the platform to 11.3. The Rainstorm target doesn't have any dependencies. We add Mockingjay as a dependency of the RainstormTests target. This is what the Podfile should look like.

platform :ios, '11.3'

target 'Rainstorm' do
  use_frameworks!

  target 'RainstormTests' do
    inherit! :search_paths

    pod 'Mockingjay'
  end

end

Close Xcode and run pod install at the root of the project to install the dependencies we defined in the Podfile.

pod install

CocoaPods installs the dependencies we defined and it creates a workspace for us. It's important that we use the workspace from now on. Navigate to the root of the project in Finder and double-click Rainstorm.xcworkspace to open the Xcode workspace.

Open Xcode Workspace

Breaking the Test Suite

Open RootViewModelTests.swift and navigate to the setUp() method. If we continue to use an instance of the MockNetworkService class, then using Mockingjay won't have much of an effect. We create an instance of the NetworkManager class and pass it to the initializer of the RootViewModel class.

override func setUp() {
    super.setUp()

    // Initialize Mock Network Service
    networkService = MockNetworkService()

    // Configure Mock Network Service
    networkService.data = loadStub(name: "darksky", extension: "json")

    // Initialize Mock Location Service
    locationService = MockLocationService()

    // Initialize Root View Model
    viewModel = RootViewModel(networkService: NetworkManager(), locationService: locationService)
}

We can remove any references to the MockNetworkService instance in RootViewModelTests.swift. Run the unit tests for the RootViewModel class to see what has changed.

The test suite is broken.

Several unit tests fail, which isn't surprising since we're no longer mocking the requests the application sends to the Dark Sky API. Let's find out how Mockingjay can help us get back in the green.

Mocking Network Requests

Mockingjay has many features and options to mock network requests. We keep it simple because the application is simple. The first unit test fails because the application fetches weather data for a location with latitude 0.0 and longitude 0.0. The assertions are based on the stub we included in the test bundle.

Mockingjay makes it easy to mock the request we send to the Dark Sky API. We start by adding an import statement for Mockingjay at the top.

import XCTest
import Mockingjay
@testable import Rainstorm

class RootViewModelTests: XCTestCase {

    ...

}

In the first unit test, we load the stub we included in the test bundle.

func testRefresh_Success() {
    // Load Stub
    let data = loadStub(name: "darksky", extension: "json")

    ...

}

We then invoke Mockingjay's stub(_:_:) function. It takes two arguments. The first argument is a matcher. A matcher defines which requests should be mocked. The second argument is a builder. As the name implies, a builder builds the responses for the matching requests. We keep it simple and pass everything as the first argument to instruct Mockingjay to mock every request the application performs. As the second argument, we convert the stub to JSON by passing it to Mockingjay's jsonData(_:) function.

func testRefresh_Success() {
    // Load Stub
    let data = loadStub(name: "darksky", extension: "json")

    // Define Stub
    stub(everything, jsonData(data))

    ...

}

Let me summarize what happens. Mockingjay intercepts every request the application performs using the URLSession API. The response of those requests is the stub we included in the test bundle. Run the unit test to verify that the unit test passes. If it passes, then the request to the Dark Sky API is mocked by Mockingjay.

That looks good. The first unit test is back in the green. For the second failed unit test, testRefresh_FailedToFetchWeatherData_RequestFailed(), the request should fail. Mockingjay makes that easy too. We create an NSError instance and pass it to Mockingjay's failure(_:) function. The result is passed as the second argument of the stub(_:_:) function. Run the unit test to verify that it's back in the green.

func testRefresh_FailedToFetchWeatherData_RequestFailed() {
    // Create Error
    let error = NSError(domain: "com.cocoacasts.network", code: 1, userInfo: nil)

    // Define Stub
    stub(everything, failure(error))

    ...

}

Fixing the third failed unit test follows the same pattern we used earlier. The response of the request should be invalid. We don't need to load a stub from the test bundle. We can create an object and serialize it by passing it to Mockingjay's json(_:) function.

func testRefresh_FailedToFetchWeatherData_InvalidResponse() {
    // Load Stub
    let body = ["some":"data"]

    // Define Stub
    stub(everything, json(body))

    ...

}

There's a bit more work to get the last failing unit test back in the green. The stub(_:_:) function accepts a matcher and a builder. We can leave the matcher as is. The builder needs to return a response that indicates that the request was successful, but the request shouldn't return any data.

We can pass a closure as the second argument. The closure accepts a URLRequest as its only argument and we need to return a Response instance. The Response type is defined in the Mockingjay library. We create an HTTPURLResponse instance, passing in the URL of the request and 200 as the status code. The success case of the Response enum defines two associated values, a URLResponse instance and a Download instance. A Download instance defines the type of data that is returned by the request. We pass noContent to indicate that the request returns no data.

func testRefresh_FailedToFetchWeatherData_NoErrorNoResponse() {
    // Define Stub
    stub(everything) { (request) -> (Response) in
        // Create HTTP URL Response
        let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!

        return .success(response, .noContent)
    }

    ...
}

Run the test suite to make sure we're back in the green. That looks good.

The test suite is back in the green.

What's Next?

The Mockingjay library has many more features and options. We only scratched the surface in this episode, but I hope you can appreciate the power and flexibility of a library that mocks network requests.

In the past episodes, we explored two options to mock network requests. Which solution you adopt depends on several factors, including the complexity of the project, the project's dependencies, and your personal preference. I recommend giving both solutions a try. That should give you a better idea of which one best fits your needs and those of your project.