iOS, Docker and Consumer Driven Contract Testing with Pact

Rajat Vig
7 min readSep 9, 2016

--

Over the past few weeks, I worked on an iOS Application which used a swarm of MicroServices developed in multiple languages with the only commonality being they all run in the cloud in Docker Containers. This post is inspired off some of the efforts to write consumer driven contract tests for the iOS Application which use Services exposing a HTTP+JSON API.

Example source code for the iOS Project can be found here. The code is intended as a demo and guide for developers working on iOS Applications and struggling with similar concerns.

The primary intent of this post is to help show how to write contract tests, not build an iOS Application. The code at Github is akin to a Client SDK that communicates with the backend service. As it stands, the demo project can be used by CocoaPods or Carthage when making the iOS Application.

What is Consumer Driven Contract Testing

At the most basic level, the Consumer runs a suite of integration tests against a Provider of services. The end result of the suite is a contract that the provider can then use to verify that it obeys the expected contract as expected by the consumer. The Pact Website does a great job explaining Consumer Driven Contract Testing, recommend very strongly reading those. Here’s links to the Wiki, FAQ and even more documentation. There are more articles that explain the nuances in further detail. 1, 2.

Why Pact

There are other tools that work equally well, notably Pacto, Mountebank. Pact has a consumer implementation in Swift which makes it a lot more suitable for iOS Applications. There also exists a Docker Image to help verify contracts on the provider end using proxies.

Tools required

  • Xcode
  • Docker

Docker Images avoid the direct dependency on Ruby and RubyGems used by almost all Pact based tools, thereby limiting them to just Docker. Using Docker Compose allows us to have commands run list on the Docker Images while providing the relevant runtime environment variables and disk volumes in a more structured manner. To install Docker

brew cask install docker

  • Make

make is ubiquitous, is readable when used correctly, has task dependencies and simplifies tasks CI/CD environments which traditionally have been done inside opaque, long winding shell scripts. It should already be installed if you’ve Xcode.

Carthage is a simpler package manager over CocoaPods. To install Carthage

brew install carthage

Swiftlint is a nice, quick easy linter for Swift code. To install Swiftlint

brew install swiftlint

Running the Demo

  • Install the required RubyGems — Fastlane, Xcov, Scan
make install
  • Run all the Unit Tests
make test
  • Run all the Contract Tests
make contract_tests
  • Publish and Verify Contracts with the Provider
make pact_publish pact_verify

This will fetch the Docker Image for the Pact Broker, Pact Verifier, Pact Publisher and the Provider. Additionally, images for Postgres (used by Pact Broker) and Redis (used by Provider) will be fetched and run.

  • Clean up
make clean

About the Provider used

The provider is akin to the many Todo Backend Services provided in multiple languages using different storage providers and frameworks to implement a HTTP+JSON API to manage Todos. I’m using a fork of a JavaScript backend which uses Koa and Redis. Feel free to use another but you will have to add support for Provider States as expected by the iOS Client.

The specific bit of the contract we are interested in is as follows

  1. Get a list of Todo Items at /todos
  2. Get a specific Todo Item at /todos/:id
  3. Create a Todo Item at /todos
  4. Deletes a specific Todo Item at /todos/:id

A JSON representation of the TodoItem is

{
"title": "blah",
"completed": false,
"order": 1,
"url": "http://localhost/todos/1"
}

The Todo Backend project documents a lot more endpoints but this is a good enough subset to get going.

Consumer code walkthrough

The model structs use SwiftyJSON to parse the JSON to Swift. The client uses Alamofire to make the HTTP calls and uses the Model classes to parse the JSON to Swift types.

Sources/Models/TodoItem.swift

public struct TodoItem: CustomStringConvertible, Equatable {
public let title: String
public let completed: Bool
public let order: Int?
public let url: NSURL?

public init(_ jsonData: JSON) {
self.title = jsonData["title"].stringValue
self.completed = jsonData["completed"].boolValue
self.order = jsonData["order"].int
self.url = NSURL.init(string: jsonData["url"].stringValue)
}
...

Sources/Models/TodoList.swift

public struct TodoList: CustomStringConvertible, Equatable {
public let todoItems: [TodoItem]

public init(_ jsonData: JSON) {
self.todoItems = jsonData.arrayValue.map { return TodoItem($0) }
}
...

Sources/TodoClient.swift

public class TodoClient {
public func getTodoList(url: NSURL, success: (TodoList) -> Void, error: () -> Void) -> Void {
...
public func getTodoItem(todoItemUrl: NSURL, success: (TodoItem) -> Void, error: () -> Void) -> Void {
...
public func deleteTodoItem(todoItemUrl: NSURL, success: () -> Void, error: () -> Void) -> Void {
let headers = [
"Accept": "application/json"
]
Alamofire.request(.DELETE, todoItemUrl, headers: headers)
.validate()
.response { request, response, data, reqerror in
guard reqerror == nil else {
print("error while deleting product: \(reqerror)")
error()
return
}
success()
}

Targets

Schemes that are available to run

  1. TodoClient which builds the TodoClient.framework.
  2. TodoClientUnitTests which runs all the Unit Tests using Quick, Nimble and OHHTTPStubs.
  3. TodoClientContractTests which runs all the contract tests using Quick, Nimble and Pact Consumer Swift

Creating the Contract Tests

Basic structure of the contract tests is simple.

  1. Give the provide a state — .given(“some state”)
  2. Defined the expectation — .uponReceiving(“a request”)
  3. Specify the exact Request — .withRequest(method: , path:, headers:, body: )
  4. Specify the expected Response — .willRespondWith(status: , headers:, body: )

The response body can use constructs like

  1. Matcher.eachLike — an array with at least element matching the given structure. The example value is what the Test will get.
  2. Matcher.somethingLike — match the datatype of the example value. The example value is what the Test will get.

Tests/TodoClientContractSpec.swift

Global Setup

var todoBackendService: MockService?
var todoClient: TodoClient?

beforeEach {
todoBackendService = MockService(
provider: "TodoBackendService",
consumer: "TodoiOSClient"
)

todoClient = TodoClient()
}
Contract Test Code for Get all Todo Items
Contract for Get all Todo Items
Contract Test Code for Get a Todo Item
Contract for Get a Todo Item
Contract Test Code for Create a Todo Item
Contract for Create a Todo Item
Contract Test Code for Delete a Todo Item
Contract for Delete a Todo Item

Generating the Contract

The scheme for TodoClientContractTests uses Docker Compose to run the Pact Mock Service. At the end of the Tests run, the generated contract is stored in the Pacts directory as declared in the compose file.

version: '2'
volumes:
pacts:
services:
pactservice:
image: rajatvig/pactservice:0.1.1
volumes:
- ./Pacts:/pacts
ports:
- "1234:80"

Running

make contract_tests

should run all the contract tests and when successful drop the contracts to the Pacts folder.

Using the Pact Broker

Pact Broker is a nice service to store generated contracts with pretty user facing documentation. It serves as the storage for all generated contracts between Consumers and Providers with support for Versioning.

Compose configuration for running the Pact Broker

version: '2'
volumes:
brokerdata:
services:
pact_broker:
image: rajatvig/pactbroker:0.1.3
hostname: broker
domainname: docker.local
environment:
PACT_BROKER_DATABASE_USERNAME: pactbroker
PACT_BROKER_DATABASE_PASSWORD: password
PACT_BROKER_DATABASE_HOST: brokerdb.docker.local
PACT_BROKER_DATABASE_NAME: pactbroker
ports:
- "80:80"
depends_on:
- pact_broker_db
links:
- pact_broker_db:brokerdb.docker.local
pact_broker_db:
image: postgres
hostname: brokerdb
domainname: docker.local
environment:
POSTGRES_PASSWORD: password
POSTGRES_USER: pactbroker
POSTGRES_DB: pactbroker
volumes:
- brokerdata:/var/lib/postgresql/data
ports:
- "5432:5432"

Running this via Docker Compose will start the Pact Broker at port 80 and a Postgres at port 5432.

Publishing the Contract

The contract when created needs to be published so it could be verified on the Provider end.

Compose configuration used to publish the contract

version: '2'
services:
pact_broker_client:
image: rajatvig/pactbroker-client:0.1.1
environment:
CONSUMER_VERSION: 0.1.0
URI_BROKER: http://localhost
volumes:
- ./Pacts:/pacts

The Pacts directory needs to be mounted to the Pact Broker Client so it could publish it to the Pact Broker.

make pact_publish

Verifying the Contract

Compose configuration for verifying the Contract

version: '2'
services:
pact_broker_proxy:
image: dius/pact-provider-verifier-docker
hostname: proxy
domainname: docker.local
environment:
pact_urls: http://localhost/pacts/provider/TodoBackendService/consumer/TodoiOSClient/latest
provider_base_url: http://provider.docker.local:3000/
provider_states_url: http://provider.docker.local:3000/states
provider_states_active_url: http://provider.docker.local:3000/states/active
depends_on:
- provider
links:
- provider:provider.docker.local
provider:
image: rajatvig/todobackendservice:0.1.1-5
hostname: provider
domainname: docker.local
environment:
NODE_ENV: test
REDIS_URL: redis.docker.local
ports:
- "3000:3000"
depends_on:
- provider_db
links:
- provider_db:redis.docker.local
provider_db:
image: redis
hostname: redis
domainname: docker.local
ports:
- "6379:6379"

Pact verification should be done for all supported consumer (TodoiOSClient), provider (TodoBackendService) and version (latest) combinations.

make pact_verify

Provider changes walkthrough

Aside from publishing a Docker Image for the Provider to enable ease of use when running tests, some of the other changes needed in the Provider.

  • Declare supported States per consumer on /states
# Request
curl http://localhost:3000/states
# Response
{
"TodoiOSClient": [
"some todoitems exist",
"a todoitem with id 1 exists"
]
}

Provider states are supported per client

  • Support POST on /states/active
# Request
curl
-d'{"state":"some todoitems exist","consumer": "TodoiOSClient"}' \
-H 'Content-Type: application/json' \
-X POST "http://localhost:3000/states/active"

Extras

  1. The project uses Fastlane to define lanes and uses appropriate Fastlane Tools. Link to the Fastfile.
  2. The project has a valid PodSpec that can be published to CocoaPods.

--

--

Rajat Vig

developer, reader, ex-Thoughtworks, staff engineer @etsy