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.
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.
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.
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.