Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
surpher/PactSwift
PactSwift
This framework provides a Swift DSL for generating Pact contracts.
It implements Pact Specification v3 and takes advantage of libpact_mock_server
running it "in process".
⚠️ NOTE ⚠️
PactSwift
is under heavy development. We are specifically looking into supportingarm64
,arm64e
alongsidex86_64
.
Any and all help with testing, raising issues and suggestions for improvement is more than welcome.
🚨 IMPORTANT 🚨
Due to the new Apple Silicon architecture and required tools' limited support forarm64
andarm64e
architecture,PactSwift
works on macOSx86_64
machines but running it onarch64-darwin
machines might be broken, for now!
Any and all help finding a working solution is more than welcome. See #52.
Requirements
PactSwift
uses pact_mock_server_ffi
written in Rust-lang as a git submodule. It builds a binary during a PactSwift
Build Phase and requires Rust installed on your machine:
brew install rust
cargo install cargo-lipo
or follow installation instructions available at rust-lang.
The first time PactSwift
is built on your machine it will take quite a long time due to also compiling the Rust binary. As long as the compiled binary exists in the Rust build folder, it will skip re-compiling it and build times should be much shorter.
Installation
Carthage
github "surpher/PactSwift" ~> 0.3
Please note Carthage is not too happy with Xcode 12 - https://github.com/surpher/PactSwift/issues/27.
Swift Package Manager
dependencies: [
.package(url: "https://github.com/surpher/PactSwift.git", .upToNextMajor(from: "0.3.0"))
]
Due to limitations of sharing binaries through SPM and the size of the compiled binaries there are a few extra steps to be made in order to use PactSwift
with SPM!
See pact-swift-examples for an example on how to set it up for Xcode and CI/CD.
Xcode setup - Carthage
NOTE:
This framework is intended to be used in your test target only! Do not embed it into your app bundle!
Setup Framework Build Settings
Framework Search Paths
In your test targets build settings, update Framework Search Paths
configuration to include $(PROJECT_DIR)/Carthage/Build/iOS (non-recursive)
:
Runpath Search Paths
In your test targets build settings, update Runpath Search Paths
configuration to include $(FRAMEWORK_SEARCH_PATHS)
:
Xcode setup - Swift Package Manager
Set PactSwift as a Swift Package

Define a Build Step Run Script
Set write permissions for Xcode to replace the fake binaries with the one compiled by your machine. Use the PactSwift/Scripts/BuildPhases/build-spm-dependency
script in the package folder:

Make sure you set the PATH
with location of your cargo
and rustup
.
Edit search paths
Add $BUILD_DIR/../../SourcePackages/checkouts/PactSwift/Resources
-recursive to Library Search Paths
and Frameworks Search Paths
in your test target's build settings.

PactSwift Environment variables
Edit your scheme and add PACT_OUTPUT_DIR
environment variable (Run
step) with path to the directory you want your Pact contracts to be written to. By default, Pact contracts are written to /tmp/pacts
.
⚠️ Sandboxed apps are limited in where they can write the Pact contract file. The default location is the Documents
folder in the sandbox (eg: ~/Library/Containers/com.example.your-project-name/Data/Documents
) and can not be overriden by the environment variable PACT_OUTPUT_DIR
. Look at the logs in debug area for the Pact file location.
To enable logging, edit your scheme and add PACT_ENABLE_LOGGING: true
to capture telemetry for debugging analysis using the unified logging system.

Writing Pact tests
- Instantiate a
MockService
object by defining pacticipants, - Define the state of the provider for an interaction (one Pact test),
- Define the expected
request
for the interaction, - Define the expected
response
for the interaction, - Run the test by making the API request using your API client and assert what you need asserted,
- Share the generated Pact contract file with your provider (eg: upload to a Pact Broker),
- Run
can-i-deploy
(eg: on your CI/CD) to deploy with confidence.
Example Test
import XCTest
import PactSwift
@testable import ExampleProject
class PassingTestsExample: XCTestCase {
var mockService = MockService(consumer: "Example-iOS-app", provider: "some-service")
// MARK: - Tests
func testGetUsers() {
// #1 - Define the API contract by configuring how `mockService`, and consequently the "real" API, will behave for this specific API request we are testing here
mockService
// #2 - Define the interaction description and provider state for this specific API request that we are testing
.uponReceiving("A request for a list of users")
.given(ProviderState(description: "users exist", params: ["first_name": "John", "last_name": "Tester"])
// #3 - Define the request we promise our API consumer will make
.withRequest(
method: .GET,
path: "/api/users",
headers: nil, // `nil` means we (and the API Provider) should not care about headers. If there are values there, fine, we're just not _demanding_ anything.
body: nil // same as with headers
)
// #4 - Define what we expect `mockService`, and consequently the "real" API, to respond with for this particular API request we are testing
.willRespondWith(
status: 200,
headers: nil, // `nil` means we don't care what the headers returned from the API are. If there are values in the header, fine, we're just not _demanding_ anything in the header.
body: [
"page": Matcher.SomethingLike(1), // We will use matchers here, as we normally care about the types and structure, not necessarily the actual value.
"per_page": Matcher.SomethingLike(20),
"total": ExampleGenerator.RandomInt(min: 20, max: 500),
"total_pages": Matcher.SomethingLike(3),
"data": Matcher.EachLike(
[
"id": ExampleGenerator.RandomUUID(), // We can also use example generators with Pact Spec v3
"first_name": Matcher.SomethingLike("John"),
"last_name": Matcher.SomethingLike("Tester"),
"salary": Matcher.DecimalLike(125000.00)
]
)
]
)
// #5 - Fire up our API client
let apiClient = RestManager()
// Run a Pact test and assert **our** API client makes the request exactly as we promised above
mockService.run(waitFor: 1) { [unowned self] completed in
// #6 - _Redirect_ your API calls to the address MockService runs on - replace base URL, but path should be the same
apiClient.baseUrl = self.mockService.baseUrl
// #7 - Make the API request.
apiClient.getUsers() { users in
// #8 - Test that **our** API client handles the response as expected. (eg: `getUsers() -> [User]`)
XCTAssertEqual(users.count, 20)
XCTAssertEqual(users.first?.firstName, "John")
XCTAssertEqual(users.first?.lastName, "Tester")
}
// #9 - Notify MockService we're done with our test, else your Pact test will time out.
completed()
}
}
}
// More tests for other interactions and/or provider states...
func testCreateUser() {
mockService
.uponReceiving("A request to create a user")
.given(ProviderState(description: "user does not exist", params: ["first_name": "John", "last_name": "Appleseed"])
.withRequest(
method: .POST,
path: Matcher.RegexLike("/api/group/whoopeedeedoodah/users", term: #"^/\w+/group/([a-z])+/users$"#),
body: [
"first_name": "John",
"last_name": "Appleseed",
"age": Matcher.SomethingLike(42),
"dob": Matcher.RegexLike("15-07-2001", term: #"\d{2}-\d{2}-\d{4}"#),
"trivia": [
"favourite_words": Matcher.EachLike("foo"),
"bar": Matcher.IncludesLike("baz")
]
]
)
.willRespondWith(
status: 201
)
let apiClient = RestManager()
mockService.run { completed in
// trigger your network request and assert the expectations
completed()
}
}
// etc.
}
Matching
In addition to verbatim value matching, you can use a set of useful matching objects that can increase expressiveness and reduce brittle test cases.
See Wiki page about Matchers for a list of matchers PactSwift
implements and their basic usage.
Or peek into /Sources/Matchers/.
Example Generators
In addition to verbatim value matching and some helpful matchers, you can use a set of example generators that generate random values each time you run your tests.
In some cases, dates and times may need to be relative to the current date and time, and some things like tokens may have a very short life span.
Example generators help you generate random values and define the rules around them.
See Wiki page about Example Generators for a list of example generators PactSwift
implements and their basic usage.
Or peek into /Sources/ExampleGenerators/.
Verifying your client against the service you are integrating with
If you set the PACT_OUTPUT_DIR
environment variable, your Xcode setup is correct and your tests successfully run, then you should see the generated Pact files in:
$(PACT_OUTPUT_DIR)/_consumer_name_-_provider_name_.json
.
Publish your generated Pact file(s) to your Pact Broker or a hosted service, so that your API-provider team can always retrieve them from one location, even when pacts change. Normally you do this regularly in you CI step/s.
See how you can use simple Pact Broker Client in your terminal (CI/CD) to upload and tag your Pact files. And most importantly check if you can safely deploy a new version of your app.
Objective-C support
PactSwift can be used in your Objective-C project with a couple of limitations, e.g. initializers with multiple optional arguments are limited to only one or two available initializers. See Demo projects repository for examples of Pact tests written in Objective-C.
Demo projects
See pact-swift-examples repo.
Project | Status | Dependency manager |
---|---|---|
iOS | Carthage | |
iOS (Objective-C) | Carthage | |
iOS | Swift Package Manager | |
macOS | Carthage | |
macOS | Swift Package Manager |
Contributing
See CODE_OF_CONDUCT.md
See CONTRIBUTING.md
Acknowledgements
This project takes inspiration from pact-consumer-swift and pull request Feature/native wrapper PR.
Logo and branding images provided by @cjmlgrto.