- Notifications
You must be signed in to change notification settings - Fork18
A Swift version of Pact. Implements Pact Specification Version 3.
License
surpher/PactSwift
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
This framework provides a Swift DSL for generating and verifyingPact contracts. It provides the mechanism forConsumer-Driven Contract Testing between dependent systems where the integration is based on HTTP.PactSwift
allows you to test the communication boundaries between your app and services it integrates with.
PactSwift
implementsPact Specification v3 and runs the mock service "in-process". No need to set up any external mock services, stubs or extra tools 🎉. It supports contract creation along with client verification. It also supports provider verification and interaction with a Pact broker.
Note: seeUpgrading for notes on upgrading and breaking changes.
- Enter
https://github.com/surpher/PactSwift
inChoose Package Repository search bar - Optionally set a minimum version whenChoosing Package Options
- Add
PactSwift
to yourtest target. Do not embed it in your application target.
dependencies: [ .package(url:"https://github.com/surpher/PactSwift.git", .upToNextMinor(from:"0.11.0"))]
Linux Installation Instructions
When usingPactSwift
on a Linux platform you will need to compile your ownlibpact_ffi.so
library for your Linux distribution frompact-reference/rust/pact_ffi or fetch aPact FFI Library x.y.z
frompact-reference releases.
It is important that the version oflibpact_ffi.so
you build or fetch is compatible with the header files provided byPactMockServer
. Seerelease notes for details.
See/Scripts/build_libpact_ffi
for some inspiration building libraries from Rust code. You can also go intopact-swift-examples and look into the Linux example projects. There is one for consumer tests and one for provider verification. They contain the GitHub Workflows where building a pact_ffi.so
binary and running Pact tests is automated with scripts.
When testing your project you can either setLD_LIBRARY_PATH
pointing to the folder containing yourlibpact_ffi.so
:
export LD_LIBRARY_PATH="/absolute/path/to/your/rust/target/release/:$LD_LIBRARY_PATH"swift buildswifttest -Xlinker -L/absolute/path/to/your/rust/target/release/
or you can move yourlibpact_ffi.so
into/usr/local/lib
:
mv /path/to/target/release/libpact_ffi.so /usr/local/lib/swift buildswifttest -Xlinker -L/usr/local/lib/
NOTE:
PactSwift
is intended to be used in yourtest target.- If running on
x86_64
(Intel machine) seeScripts/carthage (#3019-1,#3019-2,#3201)
- Instantiate a
MockService
object by definingpacticipants, - 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,
- When running on CI share the generated Pact contract file with your provider (eg: upload to aPact Broker),
- When automating deployments in a CI step run
can-i-deploy
and if computer says OK, deploy with confidence!
import XCTestimport PactSwift@testableimport ExampleProjectclassPassingTestsExample:XCTestCase{staticvarmockService=MockService(consumer:"Example-iOS-app", provider:"some-api-service") // MARK: - Testsfunc testGetUsers(){ // #1 - Declare the interaction's expectationsPassingTestsExample.mockService // #2 - Define the interaction description and provider state for this specific interaction.uponReceiving("A request for a list of users").given(ProviderState(description:"users exist", params:["first_name":"John","last_name":"Tester"]) // #3 - Declare what our client's request will look like.withRequest( method:.GET, path:"/api/users",) // #4 - Declare what the provider should respond with.willRespondWith( status:200, headers:nil, // `nil` means we don't care what the headers returned from the API are. body:["page":Matcher.SomethingLike(1), // We expect an Int, 1 will be used in the unit test"per_page":Matcher.SomethingLike(20),"total":ExampleGenerator.RandomInt(min:20, max:500), // Expecting an Int between 20 and 500"total_pages":Matcher.SomethingLike(3),"data":Matcher.EachLike( // We expect an array of objects["id":ExampleGenerator.RandomUUID(), // We can also use random example generators"first_name":Matcher.SomethingLike("John"),"last_name":Matcher.SomethingLike("Tester"),"renumeration":Matcher.DecimalLike(125_000.00)])]) // #5 - Fire up our API clientlet apiClient=RestManager() // Run a Pact test and assert **our** API client makes the request exactly as we promised abovePassingTestsExample.mockService.run(timeout:1){[unowned self] mockServiceURL, donein // #6 - _Redirect_ your API calls to the address MockService runs on - replace base URL, but path should be the same apiClient.baseUrl= mockServiceURL // #7 - Make the API request. apiClient.getUsers(){ usersin // #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 - Always run the callback. Run it in your successful and failing assertions! // Otherwise your test will time out.done()}}} // Another Pact test example...func testCreateUser(){PassingTestsExample.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:[ // You can use matchers and generators here too, but are an anti-pattern. // You should be able to have full control of your requests."first_name":"John","last_name":"Appleseed"]).willRespondWith( status:201, body:["identifier":Matcher.FromProviderState(parameter:"userId", value:.string("123e4567-e89b-12d3-a456-426614174000")),"first_name":"John","last_name":"Appleseed"])let apiClient=RestManager()PassingTestsExample.mockService.run{ mockServiceURL, donein // trigger your network request and assert the expectationsdone()}} // etc.}
MockService
holds all the interactions between your consumer and a provider. For each test method, a new instance ofXCTestCase
class is allocated and its instance setup is executed.That means each test has it's own instance ofvar mockService = MockService()
. Hence the reason we're using astatic var mockService
here to keep a reference to one instance ofMockService
for all the Pact tests. Alternatively you could wrap yourmockService
into a singleton.
Suggestions to improve this are welcome! Seecontributing.
References:
By default, generated Pact contracts are written to/tmp/pacts
. If you want to specify a directory you want your Pact contracts to be written to, you can pass aURL
object with absolute path to the desired directory when instantiating yourMockService
(Swift only):
MockService( consumer:"consumer", provider:"provider", writePactTo:URL(fileURLWithPath:"/absolute/path/pacts/folder", isDirectory:true))
Alternatively you can define aPACT_OUTPUT_DIR
environment variable (inRun
section of your scheme) with the path to directory you want your Pact contracts to be written into.
PactSwift
first checks whetherURL
has been provided when initializingMockService
object. If it is not provided it will check forPACT_OUTPUT_DIR
environment variable. If env var is not set, it will attempt to write your Pact contract into/tmp/pacts
directory.
Note that sandboxed apps (macOS apps) are limited in where they can write Pact contract files to. The default location seems to be theDocuments
folder in the sandbox (eg:~/Library/Containers/xyz.example.your-project-name/Data/Documents
). Setting the environment variablePACT_OUTPUT_DIR
might not work without some extra leg work tweaking various settings. Look at the logs in debug area for the Pact file location.
If your setup is correct and your tests successfully finish, you should see the generated Pact files in your nominated folder as_consumer_name_-_provider_name_.json
.
When running on CI use thepact-broker
command line tool to publish your generated Pact file(s) to yourPact Broker or a hosted Pact broker service. That way yourAPI-provider team can always retrieve them from one location, set up web-hooks to trigger provider verification tasks when pacts change. Normally you do this regularly in you CI step/s.
See how you can use a simplePact Broker Client in your terminal (CI/CD) to upload and tag your Pact files. And most importantly check if you cansafely deploy a new version of your app.
In your unit tests suite, prepare a Pact Provider Verification unit test:
- Start your local Provider service
- Optionally, instrument your API with ability to configureprovider states
- Run the Provider side verification step
To dynamically retrieve pacts from a Pact Broker for a provider with token authentication, instantiate aPactBroker
object with your configuration:
// The provider being verifiedletprovider=ProviderVerifier.Provider(port:8080)// The Pact broker configurationletpactBroker=PactBroker( url:URL(string:"https://broker.url/")!, auth: auth:.token(PactBroker.APIToken("auth-token")),providerName:"Your API Service Name")// Verification optionsletoptions=ProviderVerifier.Options( provider: provider, pactsSource:.broker(pactBroker))// Run the provider verification taskProviderVerifier().verify(options: options){ // do something (eg: shutdown the provider)}
To validate Pacts from local folders or specific Pact files use the desired case.
Examples
// All Pact files from a directoryProviderVerifier().verify(options:ProviderVerifier.Options( provider: provider, pactsSource:.directories(["/absolute/path/to/directory/containing/pact/files/"])), completionBlock:{ // do something})
// Only the specific Pact filespactsSource:.files(["/absolute/path/to/file/consumerName-providerName.json"])
// Only the specific Pact files at URLpactsSource:.urls([URL(string:"https://some.base.url/location/of/pact/consumerName-providerName.json")])
To submit the verification results, providePactBroker.VerificationResults
object topactBroker
.
Example
Set the provider version and optional provider version tags. Seeversion numbers for best practices on Pact versioning.
letpactBroker=PactBroker( url:URL(string:"https://broker.url/")!, auth:.token("auth-token"), providerName:"Some API Service", publishResults:PactBroker.VerificationResults( providerVersion:"v1.0.0+\(ProcessInfo.processInfo.environment["GITHUB_SHA"])", providerTags:["\(ProcessInfo.processInfo.environment["GITHUB_REF"])"]))
For a full working example of Provider Verification seePact-Linux-Provider
project inpact-swift-examples repository.
In addition to verbatim value matching, you can use a set of useful matching objects that can increase expressiveness and reduce brittle test cases.
SeeWiki page about Matchers for a list of matchersPactSwift
implements and their basic usage.
Or peek into/Sources/Matchers/.
In addition to matching, 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.
SeeWiki page about Example Generators for a list of example generatorsPactSwift
implements and their basic usage.
Or peek into/Sources/ExampleGenerators/.
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). SeeDemo projects repository for more examples.
_mockService=[[PFMockService alloc] initWithConsumer: @"Consumer-app" provider: @"Provider-server" transferProtocol: TransferProtocolStandard];
PF
stands for Pact Foundation.
Please feel free to raise anyissues as you encounter them, thanks.
Seepact-swift-examples for more examples of how to usePactSwift
.
See:
This project takes inspiration frompact-consumer-swift and pull requestFeature/native wrapper PR.
Logo and branding images provided by@cjmlgrto.
About
A Swift version of Pact. Implements Pact Specification Version 3.