Pact workshop hands-on demo by Waterstrong
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
consumer-blue
gradle/wrapper
images
pact-broker-ruby
pact-broker
provider-apple
provider-lemon
.gitignore
README.md
build.gradle
gradlew
gradlew.bat
settings.gradle

README.md

pact-workshop

Get Started

  1. Clone the project into your local:
    git clone https://github.com/Waterstrong/pact-workshop.git
    
  2. Open terminal and cd to project directory, run command:
    cd pact-workshop; ./gradlew idea
    
  3. Open pact-workshop.ipr file with IntelliJ IDEA
    idea pact-workshop.ipr || open pact-workshop.ipr || start pact-workshop.ipr
    
  4. Start Consumer Blue, run command in terminal 1:
    ./gradlew :consumer-blue:bootRun
    
    Debug mode parameter: --debug-jvm.
  5. Start Provider Apple, run command in terminal 2:
    cd provider-apple; java -jar wiremock-standalone-*.jar --port 8081 --verbose
    
  6. Start Provider Lemon, run command in terminal 3:
    ./gradlew :provider-lemon:bootRun
    
  7. Start Continuous Build, run command in terminal 4:
    ./gradlew build --continuous
    
  8. Click and open localhost swagger page in your browser and try it out:
    • Search Addresses -> Path parameter keyword starts with 13 or not will get different results.
    • Check Application -> Request payload transaction value yes or no will get different response.

Build Pact Consumer Steps

  1. 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'
    
  2. Run ./gradlew idea to download the new dependencies.

  3. Create rule PactProviderRuleMk2 to enable mock provider in DecisionServiceImplPactTest.java:

    @Rule
    public PactProviderRuleMk2 mockProvider = new PactProviderRuleMk2(PROVIDER_APPLE, "localhost", 8081, this);
    

    Notes: use PactHttpsProviderRuleMk2 to mock provider on https protocol.

  4. 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();
    }   
    
  5. Create Pact Test with PactVerification for Provider Apple in DecisionServiceImplPactTest.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));
    }
    
  6. 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 changed by overriding PACT_DIR="<new-directory>" in build.gradle ext{} block.

  7. 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"));
    }
    

Setup Pact Broker and publish Pact files

** 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 ?

  1. Add Pact plugin in consumer-blue/build.gradle file:
    plugins {
        id "au.com.dius.pact" version "3.6.1"
    }
    
  2. Add Gradle task pactPublish in the same gradle file with auth info (using http as demo here):
    pact {
        publish {
            pactDirectory = './target/pacts'
            pactBrokerUrl = 'http://pactuser:password@localhost'
        }
    
    }
    
    Notes: username and password should be configured as environment variable from pipeline.
  3. Run publish task in terminal:
    ./gradlew :consumer-blue:pactPublish
    
  4. Refresh the Pact Broker endpoint and see the Pact file with versions and relationships:

Build Pact Provider

Option 1: Pact JVM Provider Gradle
  1. Add Pact plugin in provider-lemon/build.gradle file:
    plugins {
        id "au.com.dius.pact" version "3.6.1"
    }
    
  2. Add Pact jvm provider dependency
    testImplementation 'au.com.dius:pact-jvm-provider_2.12:3.6.1'
    
  3. Run ./gradlew idea to download the latest dependencies.
  4. Define the pacts between consumers and providers
    pact {
        serviceProviders {
            provider_lemon {
                protocol = 'http'
                host = 'localhost'
                port = 8082
                path = '/api'
                hasPactsFromPactBroker('http://localhost', authentication: ['Basic', 'pactuser', 'password'])
            }
        }
    }
    
    Notes: More options(Local files, Runtime, S3 bucket, Pact Broker) to verify pact files, refer to page pact-jvm-provider-gradle.
  5. Run pact verify for provider
    ./gradlew :provider-lemon:pactVerify
    
  6. 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 add pact.verifier.publishResults=true in gradle.properties. Then it will update the verify status in Pact Broker.
Option 2: Pact Provider Verification

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.

  1. 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
    
  2. Install Ruby, then install pact-provider-verifier CLI:
    gem install pact-provider-verifier
    
  3. 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
    
Options 3, 4, 5...

More tips

  1. Define Provider state using .given()
  2. Verify the provider with unit test
    • Pact junit runner - Library provides ability to play contract tests against a provider service in JUnit fashionable way.
  3. 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
    
  4. 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.   
    
  5. Stay on master branch to practice workshop step by step with instructions. Switch to completed branch to get completed code demo.
  6. Feel free to ask questions, share thoughts and give feedback :)

Self-reflection Questions

  • 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?

More Readings