- Clone the project into your local:
git clone https://github.com/Waterstrong/pact-workshop.git
- Open terminal and cd to project directory, run command:
cd pact-workshop; ./gradlew idea
- Open
pact-workshop.ipr
file withIntelliJ IDEA
idea pact-workshop.ipr || open pact-workshop.ipr || start pact-workshop.ipr
- Start Consumer Blue, run command in terminal 1:
Debug mode parameter:
./gradlew :consumer-blue:bootRun
--debug-jvm
. - Start Provider Apple, run command in terminal 2:
cd provider-apple; java -jar wiremock-standalone-*.jar --port 8081 --verbose
- Start Provider Lemon, run command in terminal 3:
./gradlew :provider-lemon:bootRun
- Start Continuous Build, run command in terminal 4:
./gradlew build --continuous
- Click and open localhost swagger page in your browser and try it out:
-
Add dependency for pact consumer at line 13 in
consumer-blue/build.gradle
file:testImplementation 'au.com.dius:pact-jvm-consumer-junit_2.12:3.6.1'
-
Run
./gradlew idea
to download the new dependencies. -
Create rule
PactProviderRuleMk2
to enable mock provider inDecisionServiceImplPactTest.java
:@Rule public PactProviderRuleMk2 mockProvider = new PactProviderRuleMk2(PROVIDER_APPLE, "localhost", 8081, this);
Notes: use
PactHttpsProviderRuleMk2
to mock provider onhttps
protocol. -
Create Decision Pact (Provider Apple <--> Consumer Blue) in
DecisionServiceImplPactTest.java
:@Pact(provider = PROVIDER_APPLE, consumer = CONSUMER_BLUE) public RequestResponsePact createDecisionPact(PactDslWithProvider builder) { PactDslJsonBody expectedResponse = new PactDslJsonBody() .stringType("decision", "Rejected") .stringType("policyRule", "PR-8") .stringType("credRule", "CR-6") .booleanType("flag", true) .asBody(); PactDslJsonBody request = new PactDslJsonBody() .stringType("profileId") .stringValue("transaction", "no") .asBody(); return builder .uponReceiving("Process Decision") .path("/decision") .body(request) .method("POST") .willRespondWith() .status(200) .body(expectedResponse) .toPact(); }
-
Create Pact Test with
PactVerification
for Provider Apple inDecisionServiceImplPactTest.java
:@Test @PactVerification(PROVIDER_APPLE) public void shouldProcessDecisionToReturnRejectedGivenDecisionRequestTransactionNo() { DecisionRequest request = new DecisionRequest(); request.setProfileId("id12345"); request.setTransaction("no"); DecisionResponse response = decisionService.processDecision(request); assertThat(response.getDecision(), is("Rejected")); assertThat(response.getPolicyRule(), is("PR-8")); assertThat(response.getCredRule(), is("CR-6")); assertThat(response.isFlag(), is(true)); }
-
Run the tests, it will generate a pact file under
target/pacts/
default directory. It looks like:{ "provider": { "name": "provider_apple" }, "consumer": { "name": "consumer_blue" }, "interactions": [ { "description": "Process Decision", "request": { "method": "POST", "path": "/decision", "body": { ... }, "matchingRules": { ... }, ... }, "response": { "status": 200, "body": { ... }, "matchingRules": { "body": { ... }, "header": { ...} }, ... }, "providerStates": [ ... ] } ], "metadata": { ... } }
The Pact file directory can be overwritten with the
pact.rootDir
system property. This property needs to be set on the test JVM as most build tools will fork a new JVM to run the tests.test { systemProperties['pact.rootDir'] = "$buildDir/pacts" }
-
Do one more example to practice with similar above steps in
AddressServiceImplPactTest.java
:@Rule public PactProviderRuleMk2 mockProvider = new PactProviderRuleMk2(PROVIDER_LEMON, "localhost", 8082, this); @Pact(provider = PROVIDER_LEMON, consumer = CONSUMER_BLUE) public RequestResponsePact createAddressPact(PactDslWithProvider builder) { PactDslJsonBody expectedResponse = new PactDslJsonBody() .array("addresses") .stringType("1304/7 Riverside Quay, VIC 3006") .stringType("1305/8 Riverside Quay, VIC 3006") .closeArray() .asBody(); return builder .uponReceiving("Search Addresses") .path("/addresses") .query("keyword=13 Riverside") .method("GET") .willRespondWith() .status(200) .body(expectedResponse) .toPact(); } @Test @PactVerification(PROVIDER_LEMON) public void shouldSearchAddressesGivenAddressKeyword() { AddressResponse addressResponse = addressService.searchAddresses("13 Riverside"); assertThat(addressResponse.getAddresses(), hasSize(2)); assertThat(addressResponse.getAddresses().get(0), is("1304/7 Riverside Quay, VIC 3006")); }
How to setup Pact Broker service ?
There are few options to setup Pact Broker: e.g. Hosted Pact Broker, Pact Broker with Ruby, Terraform on AWS, Pact Broker Openshift and Pact Broker Docker container. Choose one of the options to try.
For this workshop, we'd like to use docker container solution to setup Pact Broker. For more instructions, refer to Dockerised Pact Broker and Pact Foundation Pact Broker.
If your team is using AWS, the Dockerised Pact Broker with AWS RDS Postgres database solution is recommended.
For this workshop demo, the docker-compose.yml is ready for quick start-up.
cd pact-broker; docker-compose up
Check if Pact Broker service is up running at both http
and https
protocols: http://localhost/, https://localhost:8443/.
The username and password are readonly:password
(developer readonly) and pactuser:password
(pipeline publish)
How to publish consumer Pact files to Pact Broker ?
- Add Pact plugin in
consumer-blue/build.gradle
file:plugins { id "au.com.dius.pact" version "3.6.1" }
- Add Gradle task
pactPublish
in the same gradle file with auth info (using http as demo here):Notes: username and password should be configured as environment variable from pipeline.pact { publish { pactDirectory = './target/pacts' pactBrokerUrl = 'http://pactuser:password@localhost' } }
- Run publish task in terminal:
./gradlew :consumer-blue:pactPublish
- Refresh the Pact Broker endpoint and see the Pact file with versions and relationships:
- Add Pact plugin in
provider-lemon/build.gradle
file:plugins { id "au.com.dius.pact" version "3.6.1" }
- Add Pact jvm provider dependency
testImplementation 'au.com.dius:pact-jvm-provider_2.12:3.6.1'
- Run
./gradlew idea
to download the latest dependencies. - Define the pacts between consumers and providers
Notes: More options(Local files, Runtime, S3 bucket, Pact Broker) to verify pact files, refer to page pact-jvm-provider-gradle.
pact { serviceProviders { provider_lemon { protocol = 'http' host = 'localhost' port = 8082 path = '/api' hasPactsFromPactBroker('http://localhost', authentication: ['Basic', 'pactuser', 'password']) } } }
- Run pact verify for provider
./gradlew :provider-lemon:pactVerify
- As you will get the warning message
Skipping publishing of verification results as it has been disabled (pact.verifier.publishResults is not 'true')
. To fix it by adding the gradle parameter-Ppact.verifier.publishResults=true
or addpact.verifier.publishResults=true
ingradle.properties
. Then it will update the verify status in Pact Broker.
Pact Provider Verification - This setup simplifies Pact Provider verification process in any language, wrapping the Ruby implementation into a cross-platform, binary-like CLI tool. It seems that the CLI is not parsing matcher rules correctly. The pactVerify task can also be used on consumer side.
- Make sure the Provider Apple Wiremock service is running. To refresh the wiremock mapping data by running command:
curl -X POST http://localhost:8081/__admin/mappings/reset --verbose
- Install Ruby, then install
pact-provider-verifier
CLI:gem install pact-provider-verifier
- Run the following command to verify Provider Apple:
pact-provider-verifier --pact-broker-base-url=http://localhost --broker-username=pactuser --broker-password=password --provider-base-url=http://localhost:8081/api --provider=provider_apple --provider-app-version=1.0.0 --publish-verification-results=true --verbose
- Define Provider state using
.given()
- Verify the provider with unit test
- Pact junit runner - Library provides ability to play contract tests against a provider service in JUnit fashionable way.
- Try heart beat and badge endpoint
http://localhost/diagnostic/status/heartbeat https://localhost:8443/diagnostic/status/heartbeat http://localhost/pacts/provider/provider_lemon/consumer/consumer_blue/latest/badge.svg https://localhost:8443/pacts/provider/provider_lemon/consumer/consumer_blue/latest/badge.svg
- Define Pact file version
version = "1.0.0" // In xxx/build.gradle or within pact block. It will use version of the gradle project by default.
- Stay on
master
branch to practice workshop step by step with instructions. Switch tocompleted
branch to get completed code demo. - Feel free to ask questions, share thoughts and give feedback :)
- Can I explain the CDC(Consumer Driven Contract Testing) ?
- Can I explain the differences between contract test and functional test ?
- Do I know one or two tools for contract testing ?
- Do I know how to write script to generate Pact file?
- Do I know how to verify provider in Pact ?
- Do I know what is Pact Broker ?
- Do I know how to publish the Pact file ?
- Do I know the best practices for Contract Testing?
- Pact Docs - docs.pact.io
- Closing the loop with Pact verifications
- Trust but verify. Using Pact for contract testing
- Consumer-Driven Contracts using a Pact broker
- Advanced Contract Testing - Pact Verification with Pattern Matching
- Consumer Driven Contract Tests
- Contract Tests vs Functional Tests
- Consumer-Driven Contracts: A Service Evolution Pattern
- Understanding Contract Testing for Microservices