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
- Get a list of Todo Items at /todos
- Get a specific Todo Item at /todos/:id
- Create a Todo Item at /todos
- 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.
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)
}
...
public struct TodoList: CustomStringConvertible, Equatable {
public let todoItems: [TodoItem]
public init(_ jsonData: JSON) {
self.todoItems = jsonData.arrayValue.map { return TodoItem($0) }
}
...
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
- TodoClient which builds the TodoClient.framework.
- TodoClientUnitTests which runs all the Unit Tests using Quick, Nimble and OHHTTPStubs.
- 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.
- Give the provide a state — .given(“some state”)
- Defined the expectation — .uponReceiving(“a request”)
- Specify the exact Request — .withRequest(method: , path:, headers:, body: )
- Specify the expected Response — .willRespondWith(status: , headers:, body: )
The response body can use constructs like
- Matcher.eachLike — an array with at least element matching the given structure. The example value is what the Test will get.
- Matcher.somethingLike — match the datatype of the example value. The example value is what the Test will get.
Global Setup
var todoBackendService: MockService?
var todoClient: TodoClient?
beforeEach {
todoBackendService = MockService(
provider: "TodoBackendService",
consumer: "TodoiOSClient"
)
todoClient = TodoClient()
}
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"