Radu Dan
Stubbing your network responses with WireMock

Stubbing your network responses with WireMock

Motivation

I was looking for an alternative to OHTTPStubs to use in my unit tests for network responses and WireMock catch my eyes. It is written in Java, can be run as a standalone process, is simple to use and I don't make my project/app dependent on a 3rd party by importing it in my unit test files.

Stubbing vs Mocking

A stub is something that has a predefined behaviour. You know that if you call method X, you will get result Y. For each of our API requests, we will have JSONs that will be sent as responses.

Example:

POST /users {"firstName": "John", "lastName": "Smith"}

And the JSON file register-stub.json will always return as part of the response 200 OK

On the other hand, a mock is what you will setup as part of the expectations.

Example:

          
            struct EndpointMock: Endpoint {
                var path: String
                var queryItems: [URLQueryItem]? = nil
                var scheme: String? = "http"
                var host: String? = "localhost"
                var port: Int? = 9999

                init(path: String) {
                    self.path = path
                }
            }

            enum URLSessionMockFactory {
                static func makeSession() -> URLSession {
                  let urlSession = URLSession(configuration: .ephemeral)
                  return urlSession
            }

What is WireMock

WireMock is an open-source HTTP server written in Java that helps you create stubs for your Network requests and is very useful for tests. For more info, you can check their page.

It has two powerful features:

  • It can be used as a library by any JVM application, or run as a standalone process either on the same host as the system under test or a remote server.
  • All of WireMock’s features are accessible via its REST (JSON) interface and its Java API. Additionally, stubs can be configured via JSON files.

Running WireMock as a standalone process

First you need to download the standalone JAR from their website.

To start the server just open a Terminal window and enter this command:

java -jar wiremock-standalone-VERSION-YOU-DOWNLOADED.jar

There are some useful parameters that you can use when running the command:

  • port - You select the port you want WireMock to run (e.g. 9999)
  • verbose - Turn on the verbose logging
  • record-mappings - It will record all incoming requests
  • proxy-all - All requests will be proxied to another URL
  • print-all-network-traffic - It will display in console all incoming and outgoing network traffic

For more commands use --help.

Stubbing responses

You create and save your stubs in the root mappings folder. You can additionally use "__files" folder to add your JSONs and configure in mappings to serve that file or not.

Example mappings/config-201-post-user-register.json

          
          {
              "request": {
                  "method": "POST",
                  "url": "/api/users/success",
                  "headers": {
                    "Accept" : {
                      "equalTo" : "application/json"
                    },
                    "Content-Type": {
                      "equalTo" : "application/json"
                    }
                  },
                  "bodyPatterns": [{
                    "equalToJson": "{\"username\": \"demo-user\", \"password\": \"41bd876b085d6031cb0e04de35b88d77f83a4ba39f879fee40805ac19e356023\"}"
                  }]
              },
              "response": {
                  "status": 201,
                  "headers": {
                      "Content-Type": "application/json; charset=utf-8",
                      "Location": "/api/users/939C0E30-7C25-436D-9AC6-571C2E339AB7"
                  }
              }
          }

Example mappings.user-login

          
          {
              "request": {
                  "method": "POST",
                  "url": "/api/users/login/success",
                  "headers": {
                    "Accept" : {
                      "equalTo" : "application/json"
                    },
                    "Content-Type": {
                      "equalTo" : "application/json"
                    }
                  },
                  "basicAuthCredentials" : {
                    "username" : "demo-user",
                    "password" : "41bd876b085d6031cb0e04de35b88d77f83a4ba39f879fee40805ac19e356023"
                  }
              },
              "response": {
                  "status": 200,
                  "headers": {
                      "Content-Type": "application/json; charset=utf-8"
                  },
                  "bodyFileName": "happy-path/response-200-users-login.json"
              }
          }

And __files/happy-path/response-200-users-login.json:

          
          {
              "id": "5D38234E-D67D-4DCF-BDB4-B9B7D21BA092",
              "token": "v2s4o0XcRgDHF/VojbAmGQ==",
              "userID": "07C3E7A9-7B0B-4CD8-97E0-93AEC7093862"
          }

Practical use case

Imagine that you finished developing your Networking framework and now you want to write unit tests for it.

The standard endpoint of your application looks like this:

          
          struct StandardEndpoint: Endpoint {
              var path: String
              var queryItems: [URLQueryItem]? = nil
              var scheme: String? = "https"
              var host: String? = "foo.com"
              var port: Int? = nil

                init(path: String) {
                    self.path = path
                }
}

You create your Mocks (EndpointMock and URLSessionMockFactory) as defined in the previous section. The tests file can be defined as below:

          
            import XCTest
            @testable import FootballGather

            final class LoginServiceTests: XCTestCase {

                private let session = URLSessionMockFactory.makeSession()
                private let resourcePath = "/api/users/login"
                private let appKeychain = AppKeychainMockFactory.makeKeychain()

                override func tearDown() {
                    appKeychain.storage.removeAll()
                    super.tearDown()
                }

                func test_request_completesSuccessfully() {
                    let endpoint = EndpointMockFactory.makeSuccessfulEndpoint(path: resourcePath)
                    let service = LoginService(session: session,
                                               urlRequest: StandardURLRequestFactory(endpoint: endpoint),
                                               appKeychain: appKeychain)
                    let user = ModelsMockFactory.makeUser()
                    let exp = expectation(description: "Waiting response expectation")

                    service.login(user: user) { [weak self] result in
                        switch result {
                        case .success(let success):
                            XCTAssertTrue(success)
                            XCTAssertEqual(self?.appKeychain.token!, ModelsMock.token)
                            exp.fulfill()
                        case .failure(_):
                            XCTFail("Unexpected failure")
                        }
                    }

                    wait(for: [exp], timeout: TestConfigurator.defaultTimeout)
                }

                // other methods
             }

Where:

  • session - Is a mocked URLSession having an ephemeral configuration
  • resourcePath - Is used for creating the endpoint URL for the User resources
  • appKeychain - Is a mocked storage that acts as a Keychain for holding the token passed in the requests, after authentication. All data is hold in memory in a cache dictionary.

In the test method, we define the mocks we are going to use in the login method.

The actual method is defined below:

              
              func login(user: UserRequestModel, completion: @escaping (Result<Bool, Error>) -> Void) {
                  var request = urlRequest.makeURLRequest()
                  request.httpMethod = "POST"

                  let basicAuth = BasicAuth(username: user.username, password: Crypto.hash(message: user.password)!)
                  request.setValue("Basic \(basicAuth.encoded)", forHTTPHeaderField: "Authorization")

                  session.loadData(from: request) { [weak self] (data, response, error) in
                      if let error = error {
                          completion(.failure(error))
                          return
                      }

                      guard let data = data, data.isEmpty == false else {
                          completion(.failure(ServiceError.expectedDataInResponse))
                          return
                      }

                      guard let loginResponse = try? JSONDecoder().decode(LoginResponseModel.self, from: data) else {
                          completion(.failure(ServiceError.unexpectedResponse))
                          return
                      }

                      self?.appKeychain.token = loginResponse.token

                      completion(.success(true))
                  }
              }

We pass the username and the hash of the password of the user in the header of the request, encoded in base64 and combined with a semicolon (:). Please read the basic access authentication principle.

We receive a generated UUID (token) that will be sent in further network calls. This variable will be stored in the user's keychain and it should expire after a period of time.

Our unit test covers just the happy path, we verify that the request was completed successfully and the token is now stored in the app's keychain.

Starting and stopping WireMock

Now we want to start WireMock before the unit test setUp and make sure that WireMock is terminated after all of our tests finish to execute.

Starting WireMock

Go to Xcode, navigate to your project scheme and click edit. On left panel, navigate to Test and select Pre-actions. Enter the script from below:

java -jar "${SRCROOT}/../stubs/wiremock-standalone-2.22.0.jar" --port 9999 --root-dir "${SRCROOT}/../stubs" 2>&1 &

In our example the folder structure is like this:

Stopping WireMock

In the Post-actions section, make sure you close the server by executing doing a POST to ../__admin/shutdown:

curl -X POST http://localhost:9999/__admin/shutdown Folder sturcutre

Run and see your UT passing.

Integrate it as part of your CI

In my example project, I am using travis as a CI service https://travis-ci.com/.

The yml file for configuration:

                  
                  osx_image: xcode10.2
                  language: swift
                  before_script: ./scripts/start-wiremock.sh
                  script: ./scripts/run-tests.sh
                  after_script: ./scripts/stop-wiremock.sh

And the scripts can be found here.

Conclusion

We have seen a basic use for WireMock, where we create a response stub for a successful login scenario. For more examples, you can check my repo with the example app that you saw some snippets:

Networking UT:

https://github.com/radude89/footballgather-ios/tree/master/FootballGather/FootballGatherTests/Networking

Stubs:

https://github.com/radude89/footballgather-ios/tree/master/stubs

WireMock is very powerful, you can simulate network conditions, failures or errors and you even can define your response patterns/regexes. More, you can make use of console output and the logging capabilities.

References