This library provides a Swift / Objective C DSL for creating Consumer Pacts.
Why? To test communication boundaries between your app and services. You can view a presentation on how Pact can work in a mobile context here: Yow! Connected 2016 Andrew Spinks - Increasing The Confidence In Your Service Integrations.
Implements Pact Specification v2, including flexible matching.
This DSL relies on the Ruby pact-mock_service gem to provide the mock service for the tests.
Note: see Upgrading for notes on upgrading from 0.2 to 0.3
Install the pact-mock_service
Run sudo gem install pact-mock_service -v 2.1.0
in your terminal.
In Xcode, edit your scheme and add pre- and post-actions for your Test
step to run the provided scripts in ./scripts/
folder. Make sure you select your target in Provide build settings from the drop down menu.
# Examples:
# Pre-actions
PATH=/full/path/to/your/rubies/bin:$PATH
"$SRCROOT"/scripts/start_server.sh
# Post-actions
PATH=/full/path/to/your/rubies/bin:$PATH
"$SRCROOT"/scripts/stop_server.sh
Using Carthage library manager
- See the PactSwiftExample for an example project using
pact-consumer-swift
with Carthage for an iOS target. - See the PactMacOSExample for an example project using
pact-consumer-swift
through Carthage for a macOS target.
Using CocoaPods (Git Submodules)
- See the PactObjectiveCExample for an example project using
pact-consumer-swift
with CocoaPods for an iOS target.
Using Swift Package Manager dependencies manager
- See the PactSwiftPMExample for an example project using
pact-consumer-swift
library through Swift Package Manager for an executable file that runs in terminal.
Write a Unit test similar to the following (NB: this example is using the Quick test framework)
import PactConsumerSwift
...
beforeEach {
animalMockService = MockService(provider: "Animal Service", consumer: "Animal Consumer Swift")
animalServiceClient = AnimalServiceClient(baseUrl: animalMockService!.baseUrl)
}
it("gets an alligator") {
animalMockService!.given("an alligator exists")
.uponReceiving("a request for an alligator")
.withRequest(method:.GET, path: "/alligator")
.willRespondWith(status:200,
headers: ["Content-Type": "application/json"],
body: ["name": "Mary"])
//Run the tests
animalMockService!.run { (testComplete) -> Void in
animalServiceClient!.getAlligator { (alligator) in
expect(alligator.name).to(equal("Mary"))
testComplete()
}
}
}
An optional timeout
(seconds) parameter can be included on the run function. This defaults to 30 seconds.
...
animalMockService!.run(timeout: 60) { (testComplete) -> Void in
animalServiceClient!.getAlligator { (alligator) in
expect(alligator.name).to(equal("Mary"))
testComplete()
}
}
Write a Unit test similar to the following
@import PactConsumerSwift;
...
- (void)setUp {
[super setUp];
self.animalMockService = [[MockService alloc] initWithProvider:@"Animal Provider"
consumer:@"Animal Service Client Objective-C"];
self.animalServiceClient = [[OCAnimalServiceClient alloc] initWithBaseUrl:self.animalMockService.baseUrl];
}
- (void)testGetAlligator {
typedef void (^CompleteBlock)();
[[[[self.animalMockService given:@"an alligator exists"]
uponReceiving:@"oc a request for an alligator"]
withRequestHTTPMethod:PactHTTPMethodGET
path:@"/alligator"
query:nil headers:nil body:nil]
willRespondWithHTTPStatus:200
headers:@{@"Content-Type": @"application/json"}
body: @"{ \"name\": \"Mary\"}" ];
[self.animalMockService run:^(CompleteBlock testComplete) {
Animal *animal = [self.animalServiceClient getAlligator];
XCTAssertEqualObjects(animal.name, @"Mary");
testComplete();
}];
}
An optional timeout
(seconds) parameter can be included on the run function. This defaults to 30 seconds.
...
[self.animalMockService run:^(CompleteBlock testComplete) {
Animal *animal = [self.animalServiceClient getAlligator];
XCTAssertEqualObjects(animal.name, @"Mary");
testComplete();
} timeout:60];
}
Write a Unit Test similar to the following:
import PactConsumerSwift
...
var animalMockService: MockService?
var animalServiceClient: AnimalServiceClient?
override func setUp() {
super.setUp()
animalMockService = MockService(provider: "Animal Provider", consumer: "Animal Service Client")
animalServiceClient = AnimalServiceClient(baseUrl: animalMockService!.baseUrl)
}
func testItGetsAlligator() {
// Prepare the expecated behaviour using pact's MockService
animalMockService!
.given("an alligator exists")
.uponReceiving("a request for alligator")
.withRequest(method: .GET, path: "/alligator")
.willRespondWith(status: 200,
headers: ["Content-Type": "application/json"],
body: [ "name": "Mary" ])
// Run the test
animalMockService!.run(timeout: 60) { (testComplete) -> Void in
self.animalServiceClient!.getAlligator { (response) -> in
XCTAssertEqual(response.name, "Mary")
testComplete()
}
}
}
...
An optional timeout
(seconds) parameter can be included on the run function. Defaults to 30 seconds.
...
// Run the test
animalMockService!.run(timeout: 60) { (testComplete) -> Void in
self.animalServiceClient!.getAlligator { (response) -> in
XCTAssertEqual(response.name, "Mary")
testComplete()
}
}
In addition to verbatim value matching, you have 3 useful matching functions
in the Matcher
class that can increase expressiveness and reduce brittle test
cases.
Matcher.term(matcher, generate)
- tells Pact that the value should match using a given regular expression, usinggenerate
in mock responses.generate
must be a string.Matcher.somethingLike(content)
- tells Pact that the value itself is not important, as long as the element type (valid JSON number, string, object etc.) itself matches.Matcher.eachLike(content, min)
- tells Pact that the value should be an array type, consisting of elements like those passed in.min
must be >= 1.content
may be a valid JSON value: e.g. strings, numbers and objects.
NOTE: One caveat to note, is that you will need to use valid Ruby regular expressions and double escape backslashes.
See the PactSpecs.swift
, PactObjectiveCTests.m
for examples on how to expect error responses, how to use query params, and the Matchers.
For more on request / response matching, see Matching.
If your setup is correct and your tests run against the pack mock server, then you should see a log file here:
$YOUR_PROJECT/tmp/pact.log
And the generated pacts, here:
$YOUR_PROJECT/tmp/pacts/...
Share the generated pact file(s) with your API provider using a Pact Broker, or using the pacts in their project to test their API responses.
See Verifying pacts for more information.
- For an end to end example with a ruby back end service, have a look at the KatKit example.
- An article on using a dockerized Node.js service which uses provider states.
- The Pact website Pact
- The pact mock server that the Swift library uses under the hood Pact mock service
- A pact broker for managing the generated pact files (so you don't have to manually copy them around!) Pact broker
Please read CONTRIBUTING.md