├── consumer-ui ├── .env ├── .babelrc ├── src │ ├── client │ │ └── userDataClient.js │ ├── utils │ │ └── environment.js │ ├── index.js │ ├── App.js │ ├── userApiContract.pact.test.js │ └── serviceWorker.js ├── README.md ├── mock │ ├── __files │ │ └── data.json │ └── mappings │ │ └── callProducer.json ├── .gitignore ├── public │ └── index.html ├── package.json └── pom.xml ├── producer ├── src │ ├── main │ │ ├── resources │ │ │ ├── application.properties │ │ │ ├── banner.txt │ │ │ └── logback.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ └── demo │ │ │ ├── DemoController.kt │ │ │ ├── ProducerApplication.kt │ │ │ └── UserModel.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── example │ │ └── demo │ │ ├── ProducerApplicationTests.kt │ │ ├── UserDataProviderContractIT.kt │ │ ├── JavaUserDataProviderContractIT.java │ │ └── DemoControllerIT.kt ├── .gitignore └── pom.xml ├── consumer ├── src │ ├── test │ │ ├── resources │ │ │ ├── application.properties │ │ │ ├── __files │ │ │ │ └── data.json │ │ │ ├── mappings │ │ │ │ └── callProducer.json │ │ │ └── example-pact.json │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ └── demo │ │ │ ├── JsonToMapServiceTest.kt │ │ │ ├── ContractTest.kt │ │ │ ├── JavaContractTest.java │ │ │ └── MessagingContractTest.kt │ └── main │ │ ├── resources │ │ ├── application.properties │ │ ├── banner.txt │ │ └── logback.xml │ │ └── kotlin │ │ └── com │ │ └── example │ │ └── demo │ │ ├── messaging │ │ ├── UserDeleteEvent.kt │ │ └── UserCreateEvent.kt │ │ ├── ConsumerApplication.kt │ │ └── UserClient.kt ├── .gitignore └── pom.xml ├── .github └── FUNDING.yml ├── documentation ├── contract.jpg ├── pact-logo.png ├── js-2-server.png ├── pact_two_parts.png ├── server-2-server.png ├── broker-network-graph.png ├── uploaded-and-verified.png ├── ui-uploaded-and-verified.png ├── uploaded-but-not-verified.png ├── broker-verification-history.png ├── ui-uploaded-but-not-verified.png ├── broker-pact-details-consumer-cli.png ├── broker-pact-details-consumer-ui.png └── messaging-upoaded-but-not-verified.png ├── Contract Tests with PACT.pdf ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .travis.yml ├── .gitignore ├── docker-compose.yml ├── EXAMPLE_README.md ├── pom.xml ├── mvnw.cmd ├── mvnw └── README.md /consumer-ui/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /producer/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port = 8888 -------------------------------------------------------------------------------- /consumer/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | producer.url = http://localhost:18888 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [christian-draeger] 2 | custom: ["https://www.paypal.me/skrapeit"] 3 | -------------------------------------------------------------------------------- /consumer-ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"], 3 | "plugins": ["transform-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /consumer/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port = 8989 2 | producer.url = http://localhost:8888 3 | -------------------------------------------------------------------------------- /documentation/contract.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/contract.jpg -------------------------------------------------------------------------------- /Contract Tests with PACT.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/Contract Tests with PACT.pdf -------------------------------------------------------------------------------- /documentation/pact-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/pact-logo.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /documentation/js-2-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/js-2-server.png -------------------------------------------------------------------------------- /documentation/pact_two_parts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/pact_two_parts.png -------------------------------------------------------------------------------- /documentation/server-2-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/server-2-server.png -------------------------------------------------------------------------------- /documentation/broker-network-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/broker-network-graph.png -------------------------------------------------------------------------------- /documentation/uploaded-and-verified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/uploaded-and-verified.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip 2 | -------------------------------------------------------------------------------- /consumer-ui/src/client/userDataClient.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const fetchUserData = (url) => { 4 | return axios.get(url) 5 | }; -------------------------------------------------------------------------------- /documentation/ui-uploaded-and-verified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/ui-uploaded-and-verified.png -------------------------------------------------------------------------------- /documentation/uploaded-but-not-verified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/uploaded-but-not-verified.png -------------------------------------------------------------------------------- /documentation/broker-verification-history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/broker-verification-history.png -------------------------------------------------------------------------------- /documentation/ui-uploaded-but-not-verified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/ui-uploaded-but-not-verified.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: java 3 | 4 | services: 5 | - docker 6 | 7 | jdk: 8 | - openjdk8 9 | 10 | script: 11 | - ./mvnw clean verify -------------------------------------------------------------------------------- /consumer-ui/README.md: -------------------------------------------------------------------------------- 1 | start project (http://localhost:1234) 2 | 3 | yarn start 4 | 5 | start project with mocked producer endpoint 6 | 7 | yarn start-with-mock -------------------------------------------------------------------------------- /documentation/broker-pact-details-consumer-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/broker-pact-details-consumer-cli.png -------------------------------------------------------------------------------- /documentation/broker-pact-details-consumer-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/broker-pact-details-consumer-ui.png -------------------------------------------------------------------------------- /documentation/messaging-upoaded-but-not-verified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-draeger/pact-example/HEAD/documentation/messaging-upoaded-but-not-verified.png -------------------------------------------------------------------------------- /consumer/src/main/kotlin/com/example/demo/messaging/UserDeleteEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.messaging 2 | 3 | data class UserDeleteEvent( 4 | val timestamp: Long, 5 | val id: Long 6 | ) 7 | -------------------------------------------------------------------------------- /consumer-ui/mock/__files/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "firstName":"Mocked first name", 3 | "lastName":"Mocked last name", 4 | "age": 4711, 5 | "ids": { 6 | "id": 5050, 7 | "uuid": "Mocked someString" 8 | } 9 | } -------------------------------------------------------------------------------- /consumer-ui/src/utils/environment.js: -------------------------------------------------------------------------------- 1 | export const getEndpoint = () => { 2 | if (process.env.REACT_APP_MOCKED) { 3 | return "http://localhost:8081/user"; 4 | } 5 | return "http://localhost:8888/user" 6 | }; -------------------------------------------------------------------------------- /consumer/src/test/resources/__files/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "firstName":"Mocked first name", 3 | "lastName":"Mocked last name", 4 | "age": 4711, 5 | "ids": { 6 | "id": 5050, 7 | "uuid": "Mocked someString" 8 | } 9 | } -------------------------------------------------------------------------------- /consumer/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _____ 2 | / ___/ ___ ___ ___ __ __ __ _ ___ ____ 3 | / /__ / _ \ / _ \ (_-) { 10 | runApplication(*args) 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ -------------------------------------------------------------------------------- /consumer/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ -------------------------------------------------------------------------------- /producer/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ -------------------------------------------------------------------------------- /producer/src/test/kotlin/com/example/demo/ProducerApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import org.junit.Test 4 | import org.junit.runner.RunWith 5 | import org.springframework.boot.test.context.SpringBootTest 6 | import org.springframework.test.context.junit4.SpringRunner 7 | 8 | @RunWith(SpringRunner::class) 9 | @SpringBootTest 10 | class ProducerApplicationTests { 11 | 12 | @Test 13 | fun contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /consumer-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # node 7 | /node 8 | 9 | # testing 10 | /coverage 11 | 12 | # production - note this is different from webpack 13 | /dist 14 | /.cache 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* -------------------------------------------------------------------------------- /consumer-ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: http://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /consumer/src/main/kotlin/com/example/demo/messaging/UserCreateEvent.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo.messaging 2 | 3 | import java.util.* 4 | 5 | data class UserCreateEvent( 6 | val firstName: String, 7 | val lastName: String, 8 | val age: Int, 9 | val active: Boolean, 10 | val expiryDate: Date, 11 | val timestamp: Long, 12 | val ids: Identifiers 13 | ) 14 | 15 | data class Identifiers( 16 | val id: Long, 17 | val uuid: UUID 18 | ) -------------------------------------------------------------------------------- /producer/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | [PRODUCER] %highlight(%.-1level) %date{HH:mm:ss.SSS} %logger{45} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /consumer-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Consumer UI 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /producer/src/main/kotlin/com/example/demo/UserModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import java.time.LocalDateTime 4 | import java.util.* 5 | 6 | data class UserModel( 7 | val firstName: String = "Christian", 8 | val lastName: String = "Dräger", 9 | val age: Int = 30, 10 | val currentDate: LocalDateTime = LocalDateTime.now(), 11 | val ids: AnotherModel = AnotherModel() 12 | ) 13 | 14 | data class AnotherModel( 15 | val id: Int = 0, 16 | val uuid: String = UUID.randomUUID().toString() 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /consumer-ui/mock/mappings/callProducer.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/user" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "bodyFileName": "data.json", 9 | "headers": { 10 | "Content-Type": "application/json; charset=utf-8", 11 | "Access-Control-Allow-Origin" : "*", 12 | "Access-Control-Allow-Methods" : "*", 13 | "Access-Control-Allow-Headers": "Accept, Content-Type, Content-Encoding, Server, Transfer-Encoding", 14 | "X-Content-Type-Options" : "nosniff", 15 | "x-frame-options" : "DENY", 16 | "x-xss-protection" : "1; mode=block" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /consumer/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | [Consumer] %highlight(%.-1level) %date{HH:mm:ss.SSS} %logger{45} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | postgres: 6 | image: postgres 7 | healthcheck: 8 | test: psql postgres --command "select 1" -U postgres 9 | ports: 10 | - "5432" 11 | environment: 12 | POSTGRES_USER: postgres 13 | POSTGRES_PASSWORD: password 14 | POSTGRES_DB: postgres 15 | 16 | broker_app: 17 | image: dius/pact-broker 18 | ports: 19 | - "80:80" 20 | links: 21 | - postgres 22 | environment: 23 | PACT_BROKER_DATABASE_USERNAME: postgres 24 | PACT_BROKER_DATABASE_PASSWORD: password 25 | PACT_BROKER_DATABASE_HOST: postgres 26 | PACT_BROKER_DATABASE_NAME: postgres 27 | PACT_BROKER_LOG_LEVEL: DEBUG 28 | -------------------------------------------------------------------------------- /EXAMPLE_README.md: -------------------------------------------------------------------------------- 1 | Example Project 2 | =============== 3 | 4 | Playing around with the Example Project 5 | --------------------------------------- 6 | 7 | ### Run all tests 8 | 9 | * this will start and stop the Pact Broker automatically during test run 10 | * execute all tests (including pact verification and result publishing) 11 | 12 | mvn clean verify 13 | 14 | if you don't want the broker to shutdown run: 15 | * this will give you the opportunity to play around with the [Pact-Broker UI](http://localhost) 16 | 17 | 18 | mvn clean verify -P-broker-down 19 | 20 | ### The Broker 21 | 22 | #### start Pact-Broker manually 23 | 24 | docker compose-up 25 | 26 | #### stop Pact-Broker manually 27 | 28 | docker compose-down -------------------------------------------------------------------------------- /consumer/src/main/kotlin/com/example/demo/ConsumerApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import mu.KotlinLogging 4 | import org.springframework.boot.CommandLineRunner 5 | import org.springframework.boot.autoconfigure.SpringBootApplication 6 | import org.springframework.boot.runApplication 7 | 8 | @SpringBootApplication 9 | class ConsumerApplication( 10 | val userClient: UserClient 11 | ) : CommandLineRunner { 12 | 13 | val logger = KotlinLogging.logger {} 14 | 15 | fun main(args: Array) { 16 | runApplication(*args) 17 | } 18 | override fun run(vararg args: String?) { 19 | val dataFromProducer = userClient.callProducer() 20 | logger.warn { dataFromProducer.forEach{key, value -> println("\n$key is $value\n")} } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /consumer-ui/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {fetchUserData} from "./client/userDataClient"; 3 | import {getEndpoint} from "./utils/environment"; 4 | 5 | class App extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | data: {} 10 | }; 11 | } 12 | 13 | componentWillMount() { 14 | fetchUserData(getEndpoint()) 15 | .then(({data}) => this.setState({data})); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 |
{this.state.data.firstName}
22 |
{this.state.data.lastName}
23 |
{this.state.data.age}
24 |
25 | ); 26 | } 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /consumer/src/main/kotlin/com/example/demo/UserClient.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import khttp.get 4 | import org.json.JSONObject 5 | import org.springframework.beans.factory.annotation.Value 6 | import org.springframework.stereotype.Component 7 | 8 | @Component 9 | class UserClient( 10 | @Value("\${producer.url}") 11 | val producerUrl: String 12 | ) { 13 | 14 | fun callProducer(): Map { 15 | val producerResponse = get(producerUrl, timeout = 5.0) 16 | if (producerResponse.statusCode != 200) { 17 | throw RuntimeException("$producerUrl response was ${producerResponse.statusCode}") 18 | } 19 | return producerResponse.jsonObject.toMap() 20 | } 21 | } 22 | 23 | fun JSONObject.toMap(): Map { 24 | val map: MutableMap = linkedMapOf() 25 | this.keys().forEach { key -> map[key] = this[key].toString() } 26 | return map 27 | } 28 | -------------------------------------------------------------------------------- /consumer/src/test/kotlin/com/example/demo/JsonToMapServiceTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import org.assertj.core.api.KotlinAssertions 4 | import org.json.JSONObject 5 | import org.junit.Test 6 | 7 | class JsonToMapServiceTest { 8 | 9 | @Test 10 | fun `can parse json with string values`() { 11 | val sud = JSONObject("""{"key": "value", "number": "4711"}""").toMap() 12 | KotlinAssertions.assertThat(sud).isEqualTo(mapOf("number" to "4711", "key" to "value")) 13 | } 14 | 15 | @Test 16 | fun `can parse json with number values`() { 17 | val sud = JSONObject("""{"number": 1234, "otherNumber": 4711}""").toMap() 18 | KotlinAssertions.assertThat(sud).isEqualTo(mapOf("number" to "1234", "otherNumber" to "4711")) 19 | } 20 | 21 | @Test 22 | fun `can parse json with mixed values`() { 23 | val sud = JSONObject("""{"key": "value", "number": 4711}""").toMap() 24 | KotlinAssertions.assertThat(sud).isEqualTo(mapOf("number" to "4711", "key" to "value")) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /producer/src/test/kotlin/com/example/demo/UserDataProviderContractIT.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import au.com.dius.pact.provider.junit.Provider 4 | import au.com.dius.pact.provider.junit.State 5 | import au.com.dius.pact.provider.junit.loader.PactBroker 6 | import au.com.dius.pact.provider.junit.target.Target 7 | import au.com.dius.pact.provider.junit.target.TestTarget 8 | import au.com.dius.pact.provider.spring.SpringRestPactRunner 9 | import au.com.dius.pact.provider.spring.target.SpringBootHttpTarget 10 | import org.junit.runner.RunWith 11 | import org.springframework.boot.test.context.SpringBootTest 12 | 13 | @RunWith(SpringRestPactRunner::class) 14 | @Provider("user-data-provider") 15 | @PactBroker(protocol = "http", host = "localhost", port = "80") 16 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 17 | class UserDataProviderContractIT { 18 | @TestTarget 19 | @JvmField 20 | final val target: Target = SpringBootHttpTarget() 21 | 22 | // only needed for Ajax Consumer 23 | @State("some user available") 24 | fun userAvailable() {} 25 | } -------------------------------------------------------------------------------- /producer/src/test/kotlin/com/example/demo/JavaUserDataProviderContractIT.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | import au.com.dius.pact.provider.junit.Provider; 7 | import au.com.dius.pact.provider.junit.State; 8 | import au.com.dius.pact.provider.junit.loader.PactBroker; 9 | import au.com.dius.pact.provider.junit.target.Target; 10 | import au.com.dius.pact.provider.junit.target.TestTarget; 11 | import au.com.dius.pact.provider.spring.SpringRestPactRunner; 12 | import au.com.dius.pact.provider.spring.target.SpringBootHttpTarget; 13 | 14 | @RunWith(SpringRestPactRunner.class) 15 | @Provider("user-data-provider") 16 | @PactBroker(protocol = "http", host = "localhost", port = "80") 17 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 18 | public class JavaUserDataProviderContractIT { 19 | 20 | @TestTarget 21 | public final Target target = new SpringBootHttpTarget(); 22 | 23 | // only needed for Ajax Consumer 24 | @State("some user available") 25 | public void userAvailable() {} 26 | } 27 | -------------------------------------------------------------------------------- /producer/src/test/kotlin/com/example/demo/DemoControllerIT.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import io.restassured.RestAssured.given 4 | import org.hamcrest.Matchers.* 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | import org.springframework.boot.test.context.SpringBootTest 8 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT 9 | import org.springframework.boot.web.server.LocalServerPort 10 | import org.springframework.test.context.junit4.SpringRunner 11 | 12 | @RunWith(SpringRunner::class) 13 | @SpringBootTest(classes = [ProducerApplication::class], webEnvironment = RANDOM_PORT) 14 | class DemoControllerIT { 15 | 16 | @LocalServerPort 17 | var port: Int = 0 18 | 19 | @Test 20 | fun canCallDemoController() { 21 | given().port(port) 22 | .`when`().get("/user") 23 | .then() 24 | .statusCode(200) 25 | .body("firstName", equalTo("Christian")) 26 | .body("lastName", equalTo("Dräger")) 27 | .body("age", equalTo(30)) 28 | .body("ids.id", equalTo(0)) 29 | .body("ids.uuid", not(isEmptyOrNullString())) 30 | } 31 | } -------------------------------------------------------------------------------- /consumer-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "consumer-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.18.0", 7 | "react": "^16.6.3", 8 | "react-dom": "^16.6.3", 9 | "react-scripts-parcel": "^0.0.38" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts-parcel start", 13 | "startMocked": "REACT_APP_MOCKED=true react-scripts-parcel start", 14 | "wiremock": "wiremock --port=8081 --root-dir=./mock", 15 | "start-with-mock": "run-p -r wiremock \"startMocked\"", 16 | "build": "react-scripts-parcel build --no-cache", 17 | "test": "jest", 18 | "test:watch": "react-scripts-parcel test --env=jsdom", 19 | "eject": "react-scripts-parcel eject" 20 | }, 21 | "browserslist": { 22 | "development": [ 23 | "last 2 chrome versions", 24 | "last 2 firefox versions", 25 | "last 2 edge versions" 26 | ], 27 | "production": [ 28 | ">1%", 29 | "last 4 versions", 30 | "Firefox ESR", 31 | "not ie < 11" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "@pact-foundation/pact": "^7.0.3", 36 | "@pact-foundation/pact-node": "^6.20.0", 37 | "babel-core": "^6.26.3", 38 | "npm-run-all": "^4.1.3", 39 | "wiremock-standalone": "^2.19.0-1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /consumer/src/test/kotlin/com/example/demo/ContractTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import au.com.dius.pact.consumer.ConsumerPactTestMk2 4 | import au.com.dius.pact.consumer.MockServer 5 | import au.com.dius.pact.consumer.dsl.PactDslJsonBody 6 | import au.com.dius.pact.consumer.dsl.PactDslWithProvider 7 | import au.com.dius.pact.model.RequestResponsePact 8 | import org.assertj.core.api.KotlinAssertions.assertThat 9 | 10 | 11 | class ContractTest : ConsumerPactTestMk2() { 12 | 13 | override fun providerName(): String = "user-data-provider" 14 | override fun consumerName(): String = "user-data-cli" 15 | 16 | override fun createPact(builder: PactDslWithProvider): RequestResponsePact { 17 | 18 | val body = PactDslJsonBody() 19 | .stringType("firstName") 20 | .stringType("lastName") 21 | .numberType("age") 22 | .`object`("ids", PactDslJsonBody() 23 | .integerType("id") 24 | .uuid("uuid")) 25 | 26 | return builder.uponReceiving("can get user data from user data provider") 27 | .path("/user") 28 | .method("GET") 29 | .willRespondWith() 30 | .status(200) 31 | .body(body) 32 | .toPact() 33 | } 34 | 35 | override fun runTest(mockServer: MockServer) { 36 | val expectedKeys = listOf("firstName", "lastName", "ids", "age") 37 | val result = UserClient("${mockServer.getUrl()}/user").callProducer() 38 | assertThat(result.keys).containsAll(expectedKeys) 39 | } 40 | } -------------------------------------------------------------------------------- /consumer/src/test/kotlin/com/example/demo/JavaContractTest.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import au.com.dius.pact.consumer.ConsumerPactTestMk2; 10 | import au.com.dius.pact.consumer.MockServer; 11 | import au.com.dius.pact.consumer.dsl.PactDslJsonBody; 12 | import au.com.dius.pact.consumer.dsl.PactDslWithProvider; 13 | import au.com.dius.pact.model.RequestResponsePact; 14 | 15 | public class JavaContractTest extends ConsumerPactTestMk2 { 16 | 17 | @Override 18 | protected String providerName() { 19 | return "user-data-provider"; 20 | } 21 | 22 | @Override 23 | protected String consumerName() { 24 | return "user-data-cli"; 25 | } 26 | 27 | @Override 28 | protected RequestResponsePact createPact(PactDslWithProvider builder) { 29 | PactDslJsonBody body = new PactDslJsonBody() 30 | .stringType("firstName") 31 | .stringType("lastName") 32 | .numberType("age") 33 | .object("ids", new PactDslJsonBody() 34 | .integerType("id") 35 | .uuid("uuid") 36 | ); 37 | 38 | return builder.uponReceiving("can get user data from user data provider") 39 | .path("/user") 40 | .method("GET") 41 | .willRespondWith() 42 | .status(200) 43 | .body(body) 44 | .toPact(); 45 | } 46 | 47 | @Override 48 | protected void runTest(MockServer mockServer) { 49 | List expectedKeys = Arrays.asList("firstName", "lastName", "ids", "age"); 50 | Map result = new UserClient(mockServer.getUrl() + "/user").callProducer(); 51 | assertThat(result.keySet()).containsAll(expectedKeys); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /consumer-ui/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | consumer-ui 7 | 0.0.1-SNAPSHOT 8 | pom 9 | 10 | consumer-ui 11 | Pact example - demo consumer-ui 12 | 13 | 14 | io.github.christian-draeger 15 | pact-example 16 | 0.0.1-SNAPSHOT 17 | 18 | 19 | 20 | 21 | 22 | com.github.eirslett 23 | frontend-maven-plugin 24 | 1.6 25 | 26 | 27 | ${project.basedir} 28 | 29 | 30 | 31 | 32 | install node and yarn 33 | 34 | install-node-and-yarn 35 | 36 | 37 | v10.14.1 38 | v1.12.3 39 | 40 | 41 | 42 | 43 | yarn install 44 | 45 | yarn 46 | 47 | 48 | 49 | 50 | yarn test 51 | test 52 | 53 | yarn 54 | 55 | 56 | 57 | test 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /producer/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | producer 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | producer 11 | Pact example - demo producer application 12 | 13 | 14 | io.github.christian-draeger 15 | pact-example 16 | 0.0.1-SNAPSHOT 17 | 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-web 23 | 24 | 25 | io.rest-assured 26 | rest-assured 27 | 3.1.1 28 | test 29 | 30 | 31 | au.com.dius 32 | pact-jvm-provider-spring_2.12 33 | 3.5.23 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.apache.maven.plugins 41 | maven-failsafe-plugin 42 | 43 | 44 | true 45 | ${project.version} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | broker-down 55 | 56 | true 57 | 58 | 59 | 60 | 61 | com.dkanejs.maven.plugins 62 | docker-compose-maven-plugin 63 | 64 | 65 | down 66 | post-integration-test 67 | 68 | down 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /consumer/src/test/kotlin/com/example/demo/MessagingContractTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.demo 2 | 3 | import au.com.dius.pact.consumer.MessagePactBuilder 4 | import au.com.dius.pact.consumer.MessagePactProviderRule 5 | import au.com.dius.pact.consumer.Pact 6 | import au.com.dius.pact.consumer.PactVerification 7 | import au.com.dius.pact.model.v3.messaging.MessagePact 8 | import com.example.demo.messaging.UserCreateEvent 9 | import com.example.demo.messaging.UserDeleteEvent 10 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 11 | import io.pactfoundation.consumer.dsl.LambdaDsl.newJsonBody 12 | import org.assertj.core.api.KotlinAssertions.assertThat 13 | import org.junit.Rule 14 | import org.junit.Test 15 | import java.text.SimpleDateFormat 16 | 17 | class MessagingContractTest { 18 | 19 | @get:Rule 20 | val pactRule = MessagePactProviderRule("messaging-provider", this) 21 | 22 | private val datePattern = "yyyy-MM-dd'T'HH:mm:ss.SSSX" 23 | private val sdf = SimpleDateFormat(datePattern) 24 | private val expiryDate = sdf.parse("2017-09-16T05:25:25.000+02:00") 25 | private val aValidTimeStamp = 1544260650L 26 | 27 | @Pact(provider = "messaging-provider", consumer = "messaging-consumer") 28 | fun createEvent(builder: MessagePactBuilder): MessagePact { 29 | 30 | val body = newJsonBody { body -> 31 | body.stringType("firstName", "Christian") 32 | body.stringValue("lastName", "Draeger") 33 | body.numberType("age", 30) 34 | body.booleanType("active") 35 | body.numberValue("timestamp", aValidTimeStamp) 36 | body.time("expiryDate", datePattern, expiryDate) 37 | 38 | body.`object`("ids") { 39 | it.numberType("id", 1) 40 | it.uuid("uuid") 41 | } 42 | }.build() 43 | 44 | return builder.given("a user was created") 45 | .expectsToReceive("a create event") 46 | .withContent(body) 47 | .toPact() 48 | } 49 | 50 | @Test 51 | @PactVerification("messaging-provider", fragment = "createEvent") 52 | fun canParseCreateEvent() { 53 | val result = jacksonObjectMapper().readValue(pactRule.message, UserCreateEvent::class.java) 54 | assertThat(result.firstName).isEqualTo("Christian") 55 | assertThat(result.lastName).isEqualTo("Draeger") 56 | assertThat(result.age).isEqualTo(30) 57 | assertThat(result.active).isTrue() 58 | assertThat(result.expiryDate).isEqualTo(expiryDate) 59 | assertThat(result.timestamp).isEqualTo(aValidTimeStamp) 60 | assertThat(result.ids.id).isEqualTo(1) 61 | } 62 | 63 | @Pact(provider = "messaging-provider", consumer = "messaging-consumer") 64 | fun deleteEvent(builder: MessagePactBuilder): MessagePact { 65 | 66 | val body = newJsonBody { 67 | it.numberType("timestamp", aValidTimeStamp) 68 | it.numberType("id", 1) 69 | }.build() 70 | 71 | return builder.given("a user was deleted") 72 | .expectsToReceive("a delete event") 73 | .withContent(body) 74 | .toPact() 75 | } 76 | 77 | @Test 78 | @PactVerification("messaging-provider", fragment = "deleteEvent") 79 | fun canParseDeleteEvent() { 80 | val result = jacksonObjectMapper().readValue(pactRule.message, UserDeleteEvent::class.java) 81 | assertThat(result.timestamp).isEqualTo(aValidTimeStamp) 82 | assertThat(result.id).isEqualTo(1) 83 | } 84 | } -------------------------------------------------------------------------------- /consumer/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | consumer 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | consumer 11 | Pact example - demo consumer application 12 | 13 | 14 | 15 | 16 | jcenter 17 | https://jcenter.bintray.com/ 18 | 19 | 20 | 21 | 22 | io.github.christian-draeger 23 | pact-example 24 | 0.0.1-SNAPSHOT 25 | 26 | 27 | 28 | 18888 29 | 30 | 31 | 32 | 33 | khttp 34 | khttp 35 | 0.1.0 36 | 37 | 38 | com.fasterxml.jackson.module 39 | jackson-module-kotlin 40 | 2.9.7 41 | 42 | 43 | net.wuerl.kotlin 44 | assertj-core-kotlin 45 | 0.2.1 46 | test 47 | 48 | 49 | au.com.dius 50 | pact-jvm-consumer-java8_2.12 51 | 3.5.21 52 | test 53 | 54 | 55 | 56 | 57 | 58 | 59 | au.com.dius 60 | pact-jvm-provider-maven_2.12 61 | 3.5.11 62 | 63 | http://localhost:80 64 | ${project.version} 65 | true 66 | 67 | 68 | 69 | publish-contract 70 | pre-integration-test 71 | 72 | publish 73 | 74 | 75 | 76 | 77 | 78 | com.dkanejs.maven.plugins 79 | docker-compose-maven-plugin 80 | 81 | 82 | up 83 | test-compile 84 | 85 | up 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /consumer-ui/src/userApiContract.pact.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import { Matchers, Pact } from "@pact-foundation/pact"; 6 | import { Publisher } from "@pact-foundation/pact-node"; 7 | import path from "path"; 8 | import {fetchUserData} from "./client/userDataClient"; 9 | import packageJson from '../package.json'; 10 | 11 | const MOCK_SERVER_PORT = 4711; 12 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; 13 | 14 | describe("fetch user data", () => { 15 | 16 | // create mock server 17 | const pact = new Pact({ 18 | consumer: "user-data-ui", 19 | provider: "user-data-provider", 20 | port: MOCK_SERVER_PORT, 21 | log: path.resolve(process.cwd(), "dist/logs", "pact.log"), 22 | dir: path.resolve(process.cwd(), "dist/pacts"), 23 | logLevel: "WARN", 24 | spec: 2, 25 | cors: true 26 | }); 27 | 28 | // define contract 29 | describe("can fetch User Data", () => { 30 | beforeEach((done) => pact 31 | .setup() 32 | .then(() => { 33 | // define expected response 34 | const expectedResponse = { 35 | firstName: Matchers.like("aValidFirstName"), 36 | lastName: Matchers.like("aValidLastName"), 37 | age: Matchers.integer(100) 38 | }; 39 | 40 | // define request 41 | return pact.addInteraction({ 42 | state: "some user available", 43 | 44 | uponReceiving: "a user request", 45 | 46 | withRequest: { 47 | method: "GET", 48 | path: "/user", 49 | 50 | headers: { 51 | Accept: Matchers.term({ 52 | matcher: "application/json", 53 | generate: "application/json" 54 | }) 55 | }, 56 | }, 57 | 58 | willRespondWith: { 59 | status: 200, 60 | headers: {"Content-Type": "application/json;charset=UTF-8"}, 61 | body: expectedResponse 62 | } 63 | }); 64 | }) 65 | .then(() => done()) 66 | ); 67 | 68 | // validate contract definition 69 | it("can load user data", () => { 70 | 71 | expect.assertions(3); 72 | 73 | const url = `http://localhost:${MOCK_SERVER_PORT}/user`; 74 | 75 | const promise = fetchUserData(url); 76 | 77 | return promise.then(response => { 78 | expect(response.status).toBe(200); 79 | expect(response.headers['content-type']).toBe("application/json;charset=UTF-8"); 80 | expect(response.data).toMatchObject({ 81 | firstName: "aValidFirstName", 82 | lastName: "aValidLastName", 83 | age: 100 84 | }); 85 | }); 86 | }); 87 | }); 88 | 89 | afterAll(() => { 90 | // create contract / pact file 91 | pact.finalize(); 92 | 93 | // publish pact to broker 94 | publishContract(); 95 | }); 96 | }); 97 | 98 | const publishContract = () => { 99 | 100 | const options = { 101 | pactFilesOrDirs: [path.resolve(process.cwd(), "dist/pacts")], 102 | pactBroker: "http://localhost:80", 103 | consumerVersion: packageJson.version, 104 | tags: [packageJson.name] 105 | }; 106 | 107 | console.log("💡 trying to publish pact with following options:\n", options); 108 | 109 | new Publisher(options).publish().then(() => { 110 | console.log("✅ successfully published contract to broker") 111 | }); 112 | }; 113 | -------------------------------------------------------------------------------- /consumer/src/test/resources/example-pact.json: -------------------------------------------------------------------------------- 1 | { 2 | "provider": { 3 | "name": "user-data-provider" 4 | }, 5 | "consumer": { 6 | "name": "user-data-cli" 7 | }, 8 | "interactions": [ 9 | { 10 | "description": "can get user data from user data provider", 11 | "request": { 12 | "method": "GET", 13 | "path": "/user" 14 | }, 15 | "response": { 16 | "status": 200, 17 | "headers": { 18 | "Content-Type": "application/json; charset=UTF-8" 19 | }, 20 | "body": { 21 | "firstName": "string", 22 | "lastName": "string", 23 | "ids": { 24 | "id": 100, 25 | "uuid": "e2490de5-5bd3-43d5-b7c4-526e33f71304" 26 | }, 27 | "age": 100 28 | }, 29 | "matchingRules": { 30 | "body": { 31 | "$.firstName": { 32 | "matchers": [ 33 | { 34 | "match": "type" 35 | } 36 | ], 37 | "combine": "AND" 38 | }, 39 | "$.lastName": { 40 | "matchers": [ 41 | { 42 | "match": "type" 43 | } 44 | ], 45 | "combine": "AND" 46 | }, 47 | "$.age": { 48 | "matchers": [ 49 | { 50 | "match": "number" 51 | } 52 | ], 53 | "combine": "AND" 54 | }, 55 | "$.ids.id": { 56 | "matchers": [ 57 | { 58 | "match": "integer" 59 | } 60 | ], 61 | "combine": "AND" 62 | }, 63 | "$.ids.uuid": { 64 | "matchers": [ 65 | { 66 | "match": "regex", 67 | "regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" 68 | } 69 | ], 70 | "combine": "AND" 71 | } 72 | }, 73 | "header": { 74 | "Content-Type": { 75 | "matchers": [ 76 | { 77 | "match": "regex", 78 | "regex": "application/json;\\s?charset=(utf|UTF)-8" 79 | } 80 | ], 81 | "combine": "AND" 82 | } 83 | } 84 | }, 85 | "generators": { 86 | "body": { 87 | "$.firstName": { 88 | "type": "RandomString", 89 | "size": 20 90 | }, 91 | "$.lastName": { 92 | "type": "RandomString", 93 | "size": 20 94 | }, 95 | "$.age": { 96 | "type": "RandomInt", 97 | "min": 0, 98 | "max": 2147483647 99 | }, 100 | "$.ids.id": { 101 | "type": "RandomInt", 102 | "min": 0, 103 | "max": 2147483647 104 | }, 105 | "$.ids.uuid": { 106 | "type": "Uuid" 107 | } 108 | } 109 | } 110 | } 111 | } 112 | ], 113 | "metadata": { 114 | "pactSpecification": { 115 | "version": "3.0.0" 116 | }, 117 | "pact-jvm": { 118 | "version": "3.5.21" 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.0.5.RELEASE 9 | 10 | 11 | 12 | io.github.christian-draeger 13 | pact-example 14 | 0.0.1-SNAPSHOT 15 | pom 16 | 17 | pact-example 18 | 19 | 20 | consumer 21 | consumer-ui 22 | producer 23 | 24 | 25 | 26 | none 27 | UTF-8 28 | UTF-8 29 | 1.8 30 | 1.2.51 31 | 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter 37 | 38 | 39 | org.jetbrains.kotlin 40 | kotlin-stdlib-jdk8 41 | 42 | 43 | org.jetbrains.kotlin 44 | kotlin-reflect 45 | 46 | 47 | io.github.microutils 48 | kotlin-logging 49 | 1.5.4 50 | 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-test 55 | test 56 | 57 | 58 | 59 | 60 | ${project.basedir}/src/main/kotlin 61 | ${project.basedir}/src/test/kotlin 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-maven-plugin 66 | 67 | 68 | kotlin-maven-plugin 69 | org.jetbrains.kotlin 70 | 71 | 72 | -Xjsr305=strict 73 | 74 | 75 | spring 76 | 77 | 78 | 79 | 80 | org.jetbrains.kotlin 81 | kotlin-maven-allopen 82 | ${kotlin.version} 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-failsafe-plugin 89 | 2.22.0 90 | 91 | 92 | 93 | integration-test 94 | verify 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | com.dkanejs.maven.plugins 105 | docker-compose-maven-plugin 106 | 2.2.0 107 | 108 | ${project.basedir}/../docker-compose.yml 109 | true 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /consumer-ui/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) 17 | ); 18 | 19 | export function register(config) { 20 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 21 | // The URL constructor is available in all browsers that support SW. 22 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 23 | if (publicUrl.origin !== window.location.origin) { 24 | // Our service worker won't work if PUBLIC_URL is on a different origin 25 | // from what our page is served on. This might happen if a CDN is used to 26 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 27 | return; 28 | } 29 | 30 | window.addEventListener('load', () => { 31 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 32 | 33 | if (isLocalhost) { 34 | // This is running on localhost. Let's check if a service worker still exists or not. 35 | checkValidServiceWorker(swUrl, config); 36 | 37 | // Add some additional logging to localhost, pointing developers to the 38 | // service worker/PWA documentation. 39 | navigator.serviceWorker.ready.then(() => { 40 | console.log( 41 | 'This web app is being served cache-first by a service ' + 42 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 43 | ); 44 | }); 45 | } else { 46 | // Is not local host. Just register service worker 47 | registerValidSW(swUrl, config); 48 | } 49 | }); 50 | } 51 | } 52 | 53 | function registerValidSW(swUrl, config) { 54 | navigator.serviceWorker 55 | .register(swUrl) 56 | .then(registration => { 57 | registration.onupdatefound = () => { 58 | const installingWorker = registration.installing; 59 | installingWorker.onstatechange = () => { 60 | if (installingWorker.state === 'installed') { 61 | if (navigator.serviceWorker.controller) { 62 | // At this point, the old content will have been purged and 63 | // the fresh content will have been added to the cache. 64 | // It's the perfect time to display a "New content is 65 | // available; please refresh." message in your web app. 66 | console.log('New content is available; please refresh.'); 67 | 68 | // Execute callback 69 | if (config.onUpdate) { 70 | config.onUpdate(registration); 71 | } 72 | } else { 73 | // At this point, everything has been precached. 74 | // It's the perfect time to display a 75 | // "Content is cached for offline use." message. 76 | console.log('Content is cached for offline use.'); 77 | 78 | // Execute callback 79 | if (config.onSuccess) { 80 | config.onSuccess(registration); 81 | } 82 | } 83 | } 84 | }; 85 | }; 86 | }) 87 | .catch(error => { 88 | console.error('Error during service worker registration:', error); 89 | }); 90 | } 91 | 92 | function checkValidServiceWorker(swUrl, config) { 93 | // Check if the service worker can be found. If it can't reload the page. 94 | fetch(swUrl) 95 | .then(response => { 96 | // Ensure service worker exists, and that we really are getting a JS file. 97 | if (response.status === 404 || response.headers.get('content-type').indexOf('javascript') === -1) { 98 | // No service worker found. Probably a different app. Reload the page. 99 | navigator.serviceWorker.ready.then(registration => { 100 | registration.unregister().then(() => { 101 | window.location.reload(); 102 | }); 103 | }); 104 | } else { 105 | // Service worker found. Proceed as normal. 106 | registerValidSW(swUrl, config); 107 | } 108 | }) 109 | .catch(() => { 110 | console.log('No internet connection found. App is running in offline mode.'); 111 | }); 112 | } 113 | 114 | export function unregister() { 115 | if ('serviceWorker' in navigator) { 116 | navigator.serviceWorker.ready.then(registration => { 117 | registration.unregister(); 118 | }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![contract](documentation/contract.jpg) 2 | 3 | # Pact Example [![Build Status](https://travis-ci.org/christian-draeger/pact-example.svg?branch=master)](https://travis-ci.org/christian-draeger/pact-example) 4 | 5 | This is an example project to give an overview about **Consumer Driven Contract Testing**. 6 | 7 | ## Table of Contents 8 | * [Prolog](#prolog) 9 | * [Effective test suites with short feedback loop](#effective-test-suites-with-short-feedback-loop) 10 | * [The problem with E2E integration testing](#the-problem-with-e2e-integration-testing) 11 | * [Intro to Consumer Driven Contract Testing](#intro-to-consumer-driven-contract-testing) 12 | * [What?](#what) 13 | * [Intro to Pact](#intro-to-pact) 14 | * [The Motivation](#the-motivation-of-this-example-implementations) 15 | * [REST](#rest-example-server-to-server-communication) 16 | * [Consumer](#the-consuming-application) 17 | * [Defining a Pact](#defining-a-pact) 18 | * [Define](#define) 19 | * [Test](#test) 20 | * [Publish](#publish) 21 | * [Pact Broker intro](#the-broker) 22 | * [Broker Setup with docker-compose](#broker-setup-with-docker-compose) 23 | * [Upload contract to broker](#upload-contract-to-broker) 24 | * [Best Practices](#best-practices-(on-consumer-side)) 25 | * [Producer / Provider](#the-providing-application) 26 | * [Verify a Pact](#verify-a-pact) 27 | * [Test](#verification-test) 28 | * [Best Practices](#best-practices-(on-producers-side)) 29 | * [Javascript Consumer](#javascript-consumer) 30 | * [Define](#define-1) 31 | * [Test](#test-1) 32 | * [Publish](#publish-1) 33 | * [Spring Cloud Contract meets Pact](#spring-cloud-contract-meets-pact) 34 | * [Messaging](#messaging-example) 35 | * [Extra infos on Pact](#extra-infos-on-pact) 36 | * [Helpful links](#helpful-links) 37 | 38 | ## Prolog 39 | 40 | > When setting up a continuous deployment pipeline for any project, having a purposeful testing plan is of utmost importance. 41 | 42 | ### Effective test suites with short feedback loop 43 | 44 | An _effective_ test suite includes a combination of _testing strategies_ that leads to high test coverage, 45 | that in turn drives _confidence_. 46 | These strategies are typically in the form of unit, component, integration and acceptance tests. 47 | Monitoring and alerting too. 48 | 49 | The test suite needs to run wicked **_fast_**, and be **_reliable_**. 50 | This cannot be stressed enough. If tests are flakey (i.e. often return false negatives or positives) 51 | it cannot be depended on to give us the desired confidence that we need. 52 | 53 | Tests also need to run as early as possible in the continuous deployment pipeline to **_shorten the 54 | feedback loop_** for the developer. So ideally, most tests should be designed to run when pull requests 55 | are built (on standalone build agents that are sandboxed from the application environment). 56 | To achieve this requirement, tests must make no assumption about the availability of any of its 57 | external dependencies, and be designed to run in **_isolation_**. 58 | 59 | Tests that are designed to be fast, reliable and isolate failure will have a short and 60 | effective feedback loop that informs the developer with confidence whether the product is working or not. 61 | 62 | ### The problem with E2E integration testing 63 | 64 | Unit tests is a good example of one such testing process that achieves the desired effectiveness 65 | being aspired to — they are fast, reliable and isolate failure. 66 | However, unit tests on their own are not enough because they give no guarantee that the tested 67 | units work well together. 68 | 69 | In the absence of the perfect unit test suite, end-to-end (E2E) integration tests are typically 70 | the testing strategy of choice that is used to try and ascertain confidence in the system working 71 | as a whole. 72 | 73 | Even though E2E tests are full of promise to assert the overall product working a whole 74 | from a user’s perspective, they [quickly become a very bad idea](https://blog.thecodewhisperer.com/permalink/integrated-tests-are-a-scam) 75 | because of their [awful feedback loop](https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html) — slow, unreliable and depend on too many things at once. 76 | 77 | ### Intro to Consumer Driven Contract Testing 78 | 79 | An alternative testing strategy that complements the deficiency of unit tests but verifies that 80 | components with external dependencies work together is integration contract testing. 81 | The [concept](https://www.martinfowler.com/articles/consumerDrivenContracts.html) isn’t new, 82 | but with the mainstream acceptance of microservices, 83 | it's important to remind people that consumer-driven contracts are an essential 84 | part of a mature microservice testing portfolio, enabling independent 85 | service deployments. 86 | 87 | When two independently developed services are collaborating, 88 | changes to the supplier’s API can cause failures for all its consumers. 89 | Consuming services usually cannot test against live suppliers since such 90 | tests are slow and brittle, so it’s best to use Test Doubles (mocks), 91 | leading to the danger that the test doubles get out of sync with the real 92 | supplier service. Consumer teams can protect themselves from these failures 93 | by using integration contract tests – tests that compare actual service 94 | responses with test values. While such contract tests are valuable, 95 | they are even more useful when consuming services provide these tests to 96 | the supplier, who can then run all their consumers’ contract tests to determine 97 | if their changes are likely to cause problems. 98 | 99 | Contract testing is immediately applicable anywhere where you have two 100 | services that need to communicate - such as an API client and a web front-end 101 | or for instance to services communicating via messaging queues. 102 | Although a single client and a single service is a common use case, 103 | contract testing really shines in an environment with many services 104 | (as common for a microservice architecture). 105 | Having well-formed contract tests makes it easy for developers to avoid 106 | version hell. Contract testing is the killer app for microservice development and deployment. 107 | 108 | In general, a contract is between a consumer (for instance a client that wants 109 | to receive some data) and a provider (for instance an API on a server that 110 | provides the data the client needs). In microservice architectures, 111 | the traditional terms client and server are not always appropriate -- for example, 112 | when communication is achieved through message queues (we'll have a look at this as well). 113 | 114 | #### Benefits in short 115 | * enable services to be deployed independently 116 | * enables teams to work independently from each other 117 | * enables verification of external endpoints - am i building what is wanted? 118 | 119 | ### Intro to Pact 120 | 121 | ![pact logo](documentation/pact-logo.png) 122 | 123 | [Pact](https://docs.pact.io) is a consumer-driven contract testing tool. 124 | This means the contract is written as part of the consumer tests. 125 | A major advantage of this pattern is that only parts of the communication 126 | that are actually used by the consumer(s) get tested. 127 | This in turn means that any provider behaviour not used by current consumers 128 | is free to change without breaking tests. 129 | 130 | Pact enables consumer driven contract testing, 131 | providing a mock service and DSL for the consumer project, 132 | interaction playback and verification for the service provider project. 133 | 134 | ![pact diagram](documentation/pact_two_parts.png) 135 | 136 | The Pact family of testing frameworks 137 | (Pact-JVM, Pact Ruby, Pact .NET, Pact Go, Pact.js, Pact Swift etc.) 138 | provide support for Consumer Driven Contract Testing between dependent systems 139 | where the integration is based on HTTP (or message queues for some of the implementations). 140 | 141 | 142 | ### the motivation of this example implementations 143 | Because [Pact](https://docs.pact.io/) is supporting so much languages and different ways of doing things and 144 | they have a distributed documentation it can get messy and a bit annoying to search 145 | or better say filter for the information you particularly want / need. 146 | Only for the JVM there are currently ~20 different extensions / dependencies (plugins not included). 🤯 147 | 148 | >_**In my opinion it's absolutely awesome to get decent support for different languages and frameworks, 149 | >but it can become quite hard to keep track of all the already existing stuff (especially if you're a newbie to Pact).**_ 150 | 151 | For this reason I decided to write a compact step by step guide with working examples 152 | using Maven as build tool and provide each a Kotlin and a Java example of the test implementation. 153 | (😏 and a Javascript Consumer implementation) 154 | 155 | ## What? 156 | 157 | What's going on here (in short): 158 | * Test implementation examples: 159 | * Consumer: Kotlin, Java, Javascript 160 | * Producer: Kotlin and Java 161 | * Contract repository: [Pact Broker](#publish) 162 | * via docker-compose 163 | * Functional API tests: WireMock 164 | * Build-tool: Maven 165 | 166 | Included examples are: How to test services that are talking REST as well as examples 167 | regarding how to ensure your services that are communication via messaging providing data in the 168 | correct format (from the consumers point of view). 169 | 170 | The **REST-Example** includes two applications where one is acting as a producer 171 | (webservice with rest endpoint) and a consumer 172 | (a CLI app that prints data received from the producer to console if executed). 173 | 174 | Both of the applications (producer and consumer) are testing there-self. 175 | The Consumer-Apps dependencies (having the Producer-App available, 176 | a working internet connection and getting a suitable response) can be detached by 177 | mocking (e.g. WireMock) to run locally and independent. 178 | Great!!! so far so good. 179 | We want go a step further and decouple the release cycles of our microservices. 180 | 181 | ##### But how to make sure the Producers (supplier) response is in a Suitable format for the Consumer? 182 | 183 | 🤝 In a good relationship we know what to expect from each other and so should our services do. 184 | 185 | #### Let's make a _Pact_ 186 | 187 | What is a Pact? 🤷‍ 188 | 189 | > A formal agreement between individuals or parties. 190 | Synonyms: agreement, protocol, deal, contract 191 | >>~ Oxford Dictionaries​ 192 | 193 | In Terms of Contract Testing you should always proceed according to the following schema when implementing a Pact: 194 | 195 | * Start implementing on the Consumer side 196 | * define your contract 197 | * verify your contract against a mock 198 | * publish your contract 199 | * End up verifying the contract on Producer side 200 | 201 | > **_A few words in advance:_** 202 | >> Don't get confused by the Consumer / Producer wording - it is not related to the data flow! 203 | >> Producer in the context of Pact describes who is providing the API. 204 | >> That means for instance if you make a POST-request to an API - the API providing application is the _Producer_, where the application that is doing the request is the _Consumer_ on the other hand. 205 | 206 | 207 | Regarding the example implementations we will focus on the **[HTTP based integration](#rest-example-server-to-server-communication) first** and _later on_ we having a look at [**messaging queues**](#messaging-example). 208 | 209 | # REST Example (Server to Server communication) 210 | 211 | ![server-2-server-diagram](documentation/server-2-server.png) 212 | 213 | ###### The Consuming Application 214 | ## Defining a Pact 215 | Defining a Pact should be splitted into 3 steps: 216 | * [Define](#define) 217 | * [Test](#test) 218 | * [Publish](#publish) 219 | 220 | > If you are looking for an example on how to define a contract by using Javascript go [here](#javascript-consumer). 221 | 222 | ### Define 223 | We'll start defining our Pact at the **Consumer** Application. 224 | I mean hey, we want to work Consumer Driven and who could know its 225 | requirements regarding a producer API better then the Consumer itself? 226 | 227 | #### prerequisites on consumer side 228 | First let's add the relevant **Pact** dependency for our use-case to the consumer applications *pom.xml*. 229 | I'm using the [pact-jvm-consumer-java8](https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-consumer-java8) dependency here _(an extension for the pact DSL provided by [pact-jvm-consumer](https://github.com/DiUS/pact-jvm/blob/master/pact-jvm-consumer))_ because it 230 | provides a nice lambda based DSL for use with Junit to build consumer tests. 231 | 232 | ``` xml 233 | 234 | au.com.dius 235 | pact-jvm-consumer-java8_2.12 236 | 3.5.21 237 | test 238 | 239 | ``` 240 | 241 | Now we are able to define how the **Producer** APIs response needs to look like from the **Consumers** point of view. 242 | We'll begin by creating a test class named `ContractTest` that implements `ConsumerPactTestMk2`. 243 | 244 | You'll need to implement `providerName()`, `consumerName()`, `createPact()` and `runTest()`. 245 | 246 | The implementation of the `providerName()` method should return a string that describes the name of the provider API. 247 | Since our **Provider** is responsible for user data we should call it something like "user-data-provider": 248 | 249 | > using kotlin 250 | >``` kotlin 251 | >override fun providerName(): String = "user-data-provider" 252 | >``` 253 | 254 | > using java 255 | >``` java 256 | >@Override 257 | >protected String providerName() { 258 | > return "user-data-provider"; 259 | >} 260 | >``` 261 | 262 | The implementation of the `consumerName()` method should return a string that describes the name of the consuming service. 263 | Since our **Consumer** is an cli-tool that displays user data we should call it something like "user-data-cli": 264 | 265 | > using kotlin 266 | >``` kotlin 267 | >override fun consumerName(): String = "user-data-cli" 268 | >``` 269 | 270 | > using java 271 | >``` java 272 | >@Override 273 | >protected String consumerName() { 274 | > return "user-data-cli"; 275 | >} 276 | >``` 277 | 278 | > **Hint:** Don't test (worst-case scenario) all fields or HTTP status codes the Provider API supports with Pact. You don't want to break the Providers build on every change that's made over there (for instance if something changed that wouldn't influence the behavior on Consumer Apps side). Just be tide on the things the Consumer App really expects from the Provider API. 279 | 280 | > **Take away:** To verify how your Producer Client behaves on scenarios like network errors or getting crappy data from the API you should use WireMock. 281 | 282 | Now let's define how a request from the **Consumer** looks like and what's the 283 | expected format of the payload by implementing the `createPact()` method. 284 | 285 | We are using the `PactDslWithProvider` builder to describe the request 286 | and (because we are expecting a response with a JSON body) 287 | the `PactDslJsonBody` builder to define the payload: 288 | 289 | > using kotlin: 290 | >``` kotlin 291 | >override fun createPact(builder: PactDslWithProvider): RequestResponsePact { 292 | > 293 | > val body = PactDslJsonBody() 294 | > .stringType("firstName") 295 | > .stringType("lastName") 296 | > .numberType("age") 297 | > .`object`("ids", PactDslJsonBody() 298 | > .integerType("id") 299 | > .uuid("uuid")) 300 | > 301 | > return builder.uponReceiving("can get user data from user data provider") 302 | > .path("/user") 303 | > .willRespondWith() 304 | > .status(200) 305 | > .body(body) 306 | > .toPact() 307 | >} 308 | >``` 309 | 310 | > using java: 311 | >``` java 312 | >@Override 313 | >protected RequestResponsePact createPact(PactDslWithProvider builder) { 314 | > PactDslJsonBody body = new PactDslJsonBody() 315 | > .stringType("firstName") 316 | > .stringType("lastName") 317 | > .numberType("age") 318 | > .object("ids", new PactDslJsonBody() 319 | > .integerType("id") 320 | > .uuid("uuid") 321 | > ); 322 | > 323 | > return builder.uponReceiving("can get user data from user data provider") 324 | > .path("/user") 325 | > .method("GET") 326 | > .willRespondWith() 327 | > .status(200) 328 | > .body(body) 329 | > .toPact(); 330 | >} 331 | >``` 332 | 333 | ### Test 334 | At this point we should define our client-side test based on the defined request we 335 | described in the step before. So let our *UserClient* (that is talking to the **Provider**) 336 | call a mockServer (that is created for us by **Pact**) as we defined it in our `createPact()` implementation. 337 | 338 | >using kotlin: 339 | >``` kotlin 340 | >override fun runTest(mockServer: MockServer) { 341 | > val expectedKeys = listOf("firstName", "lastName", "ids", "age") 342 | > val result = UserClient("${mockServer.getUrl()}/user").callProducer() 343 | > assertThat(result.keys).containsAll(expectedKeys) 344 | >} 345 | >``` 346 | 347 | >using java: 348 | >``` java 349 | >@Override 350 | >protected void runTest(MockServer mockServer) { 351 | > List expectedKeys = Arrays.asList("firstName", "lastName", "ids", "age"); 352 | > Map result = new UserClient(mockServer.getUrl() + "/user").callProducer(); 353 | > assertThat(result.keySet()).containsAll(expectedKeys); 354 | >} 355 | >``` 356 | 357 | > ##### So your test class should look something like [THIS](consumer/src/test/kotlin/com/example/demo/ContractTest.kt) if you are using Kotlin afterwards. 358 | > ##### So your test class should look something like [THIS](consumer/src/test/kotlin/com/example/demo/JavaContractTest.java) if you are using Java afterwards. 359 | 360 | At this point we already achieved a lot. We verified our *UserClient* is working correctly and 361 | we created the contract definition - or better said, Pact generated one for us :) - our **Provider** will validate his Api against later on. 362 | You can have a look at it under `/target/pacts/user-data-cli-user-data-provider.json` (it should look similar to [THIS](consumer/src/test/resources/example-pact.json) one). 363 | 364 | ### Publish 365 | 366 | #### The Broker 367 | Sharing is caring - In this Example we are using a broker to host our contracts. 368 | For showcasing reasons we just start the [Pact-Broker](https://github.com/pact-foundation/pact_broker) and a postgres 369 | database via docker-compose. In a real world scenario you probably want to run the broker permanently on a VM - so you should deploy it somewhere. 370 | But because this example is focusing on Pact itself we'll proceed using docker to quickly get a running Pact broker. 371 | 372 | The Pact Broker provides a repository for consumer driven contracts that: 373 | - tells you which versions of your applications can be deployed safely together 374 | - solves the problem of how to share pacts between consumer and provider projects 375 | - allows you to decouple your service release cycles 376 | - provides API documentation that is guaranteed to be up-to date 377 | - shows you real examples of how your services interact 378 | - allows you to visualise the relationships between your services 379 | - can integrate with other systems, such as Slack or your CI server, via webhooks 380 | 381 | #### Broker Setup with docker-compose 382 | To archive a running Pact-Broker via docker-compose we put a file called `docker-compose.yml` in the root of our project. 383 | 384 | ```yaml 385 | version: '3' 386 | 387 | services: 388 | 389 | postgres: 390 | image: postgres 391 | healthcheck: 392 | test: psql postgres --command "select 1" -U postgres 393 | ports: 394 | - "5432:5432" 395 | environment: 396 | POSTGRES_USER: postgres 397 | POSTGRES_PASSWORD: password 398 | POSTGRES_DB: postgres 399 | 400 | broker_app: 401 | image: dius/pact-broker 402 | ports: 403 | - "80:80" 404 | links: 405 | - postgres 406 | environment: 407 | PACT_BROKER_DATABASE_USERNAME: postgres 408 | PACT_BROKER_DATABASE_PASSWORD: password 409 | PACT_BROKER_DATABASE_HOST: postgres 410 | PACT_BROKER_DATABASE_NAME: postgres 411 | PACT_BROKER_LOG_LEVEL: DEBUG 412 | ``` 413 | 414 | Afterwards run: 415 | 416 | $ docker-compose up 417 | 418 | >*Troubleshooting on Mac/Windows:* 419 | >I get a "initdb: could not create directory "/var/lib/postgresql/data/pg_wal": No space left on device" error 420 | > * Your docker VM has probably run out of disk space. 421 | > * Try deleting some old images. (`docker rmi -f $(docker images -a -q)`) 422 | > * check your docker config and increase its disk image size 423 | 424 | Thereby we achieved to have a Pact-Broker running (on port 80). 425 | To verify everything went well just open [http://localhost](http://localhost) in your browser. 426 | You should see the Pact-Broker UI but no uploaded contract for now. 427 | 428 | #### Upload contract to broker 429 | 430 | To upload the Contract add the following plugin to the consumers pom.xml 431 | 432 | ```xml 433 | 434 | au.com.dius 435 | pact-jvm-provider-maven_2.12 436 | 3.5.11 437 | 438 | http://localhost:80 439 | ${project.version} 440 | true 441 | 442 | 443 | ``` 444 | 445 | Afterwards you are able to upload your contract to the broker by executing the following command: 446 | 447 | $ mvn verify pact:publish 448 | 449 | If everything went well you should see your contract in the Pact-Broker UI. 450 | 451 | ![pact uploaded](documentation/uploaded-but-not-verified.png) 452 | 453 | In a real world project you should think about a suitable way to execute this command 454 | within your build chain - for instance everytime the Producer client implementation 455 | has changed on the consumer side. 456 | 457 | ### Best Practices (on Consumer side) 458 | * Use Pact for contract testing, not functional testing of the provider!!! (read more [here](https://docs.pact.io/best_practices/consumer#use-pact-for-contract-testing-not-functional-testing-of-the-provider) and [here](https://docs.pact.io/best_practices/consumer/contract_tests_not_functional_tests)) 459 | * Use Pact for isolated (unit) tests ([read more...](https://docs.pact.io/best_practices/consumer#use-pact-for-isolated-unit-tests)) 460 | * Think carefully about how you use it for non-isolated tests (functional, integration tests) ([read more...](https://docs.pact.io/best_practices/consumer#think-carefully-about-how-you-use-it-for-non-isolated-tests-functional-integration-tests)) 461 | * Always make sure you made the latest pact available to the Provider! 462 | * Ensure all calls to the Provider go through classes that have been tested with Pact. ([read more...](https://docs.pact.io/best_practices/consumer#ensure-all-calls-to-the-provider-go-through-classes-that-have-been-tested-with-pact)) 463 | 464 | # The Providing Application 465 | ###### (the Producer) 466 | ## Verify a Pact 467 | 468 | Now that we have a Contract defined by the Consumer our Provider have to verify it. 469 | On the Provider side this test should always be executed in your build-chain to make sure you 470 | are not breaking things on Consumers side. 471 | In contrast to the consumer tests, provider verification is entirely driven by the Pact framework (there's nothing better than things getting simple ❤️). 472 | 473 | ### Verify 474 | #### prerequisites on Producer side 475 | In our case, we want to start our _Spring-Boot_-based Producer app and 476 | have the Pact being checked against it. 477 | This is why we are going to use pact-jvm-provider-spring_2.12 package because 478 | it is bringing some really handy annotations into the game - you'll see what i mean in just a minute. 479 | 480 | ```xml 481 | 482 | au.com.dius 483 | pact-jvm-provider-spring_2.12 484 | 3.5.23 485 | 486 | ``` 487 | 488 | > if you're not using [Spring](http://spring.io) you should have a look here: [junit-provider](https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-junit) 489 | 490 | > if you're not using [jUnit4](https://junit.org/junit4/) or [jUnit5](https://junit.org/junit5/) you should have a look here: [maven-provider](https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-maven) 491 | 492 | > if you're using [gradle](https://gradle.org) instead of [maven](https://maven.apache.org) you should have a look here: [gradle-provider](https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-gradle) 493 | 494 | #### Verification Test 495 | 496 | The second step of our contract verification is creating a test for the 497 | Producer using a mock client based on the contract. 498 | Our provider implementation will be driven by this contract in TDD fashion. 499 | The Test implementation on the Producer side is pretty straight forward. 500 | 501 | >using kotlin: 502 | >```kotlin 503 | >@RunWith(SpringRestPactRunner::class) 504 | >@Provider("user-data-provider") 505 | >@PactBroker(protocol = "http", host = "localhost", port = "80") 506 | >@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 507 | > 508 | >class UserDataProviderContractIT { 509 | > @TestTarget 510 | > @JvmField 511 | > final val target: Target = SpringBootHttpTarget() 512 | >} 513 | >``` 514 | 515 | >using java: 516 | >```java 517 | >@RunWith(SpringRestPactRunner.class) 518 | >@Provider("user-data-provider") 519 | >@PactBroker(protocol = "http", host = "localhost", port = "80") 520 | >@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 521 | >public class JavaUserDataProviderContractIT { 522 | > 523 | > @TestTarget 524 | > public final Target target = new SpringBootHttpTarget(); 525 | >} 526 | >``` 527 | 528 | > ##### So your test class should look something like [THIS](producer/src/test/kotlin/com/example/demo/UserDataProviderContractIT.kt) if you are using Kotlin. 529 | > ##### So your test class should look something like [THIS](producer/src/test/kotlin/com/example/demo/JavaUserDataProviderContractIT.java) if you are using Java. 530 | 531 | In order to get the contract verification results replayed to the Pact-Broker 532 | it's necessary to set the property `pact.verifier.publishResults=true`. 533 | This can rather be done by setting the property via the failsafe-plugin (which we use to run our integration tests) like so: 534 | ``` 535 | 536 | org.apache.maven.plugins 537 | maven-failsafe-plugin 538 | 539 | 540 | true 541 | ${project.version} 542 | 543 | 544 | 545 | ``` 546 | 547 | Or by setting the property programmatically: 548 | ``` 549 | val props = System.getProperties() 550 | props.put("pact.verifier.publishResults", "true") 551 | System.setProperties(props) 552 | ``` 553 | 554 | If you run your tests and set the system property you should see a verified contract in the Pact-Broker UI. 555 | 556 | ![pact uploaded](documentation/uploaded-and-verified.png) 557 | 558 | A nice **Pact-Broker** feature in my opinion is the network graph that shows which services have dependencies to each other 559 | or let's better say which of them assure they there compatibility by having a Pact. 🤗 560 | 561 | ![broker-network-graph](documentation/broker-network-graph.png) 562 | 563 | When clicking on an arrow in the graph you'll see a detailed view describing the exact properties 564 | a certain consumer relies on regarding the producers API. 565 | Which is really great from Producers point of view to know it's consumers and furthermore what data they are consuming in detail. 566 | 567 | ![broker-network-graph](documentation/broker-pact-details-consumer-cli.png) 568 | 569 | ### Best Practices (on Producers side) 570 | * Ensure that the latest pact is being verified ([read more...](https://docs.pact.io/best_practices/provider#ensure-that-the-latest-pact-is-being-verified)) 571 | * Ensure that Pact verify runs as part of your CI build 572 | 573 | # Javascript Consumer 574 | 575 | As already mentioned Pact is supporting a bunch of languages. A very handy combination i saw in real world projects 576 | is defining a contract between a pure javascript based consumer (for instance a node.js app) doing ajax-requests 577 | against some backend server providing an API. 578 | 579 | ![js-2-server-diagram](documentation/js-2-server.png) 580 | 581 | In this example a basic [React](https://reactjs.org) app with [Jest](https://jestjs.io) as testing platform is assumed. 582 | Your project structure could look [as follows](https://github.com/christian-draeger/pact-example/tree/master/consumer-ui). 583 | 584 | #### Prerequisites 585 | First let's add the **pact** and **pact-node** dev dependency to the consumer applications *package.json*. 586 | 587 | > The `pact-node` dependency is needed to publish your pact file to the broker. This could also be solved by 588 | > using the pact maven or gradle plugin (depending on your build setup). But to keep the JS example a pure JS example 589 | > i decided to go the pact-node way here. 590 | 591 | ``` json 592 | "devDependencies": { 593 | "@pact-foundation/pact": "7.0.3", 594 | "@pact-foundation/pact-node": "6.20.0" 595 | } 596 | ``` 597 | 598 | ### Define 599 | Now we are able to define how the **Producer** APIs response needs to look like from the **Consumers** point of view. 600 | 601 | #### Configure 602 | First of all we want Pact to to create a mock server for us, tell pact who is the consumer and the provider and where 603 | to place generated pact files. 604 | 605 | ``` javascript 1.6 606 | import { Pact } from "@pact-foundation/pact"; 607 | 608 | const pact = new Pact({ 609 | consumer: "user-data-ui", 610 | provider: "user-data-provider", 611 | port: 4711, 612 | log: path.resolve(process.cwd(), "dist/logs", "pact.log"), 613 | dir: path.resolve(process.cwd(), "dist/pacts"), 614 | logLevel: "WARN", 615 | spec: 2, // <-- it's important to use the same pact spec verion as the producer here 616 | cors: true 617 | }); 618 | } 619 | ``` 620 | 621 | >*Troubleshooting for Jest* 622 | > I get a "SecurityError: localStorage is not available for opaque origins" error when executing tests 623 | > * The root cause is often libraries looping over all jsdom properties and adding them to the global; 624 | > even if you never use localStorage in your tests, some library or test framework you are depending on is "using" it in this manner. 625 | > Note that this looping-and-copying technique is explicitly unsupported by jsdom. 626 | > * [jest issue](https://github.com/facebook/jest/issues/6766) 627 | > * [jsdom issue](https://github.com/jsdom/jsdom/issues/2304) 628 | > 629 | > To fix this you can set env config for non-browser environment by adding the following at the top of your test class: 630 | >``` javascript 1.6 631 | >/** 632 | > * @jest-environment node 633 | > */ 634 | >``` 635 | 636 | > **Hint:** Don't test (worst-case scenario) all fields or HTTP status codes the Provider API supports with Pact. You don't want to break the Providers build on every change that's made over there (for instance if something changed that wouldn't influence the behavior on Consumer Apps side). Just be tide on the things the Consumer App really expects from the Provider API. 637 | 638 | > **Take away:** To verify how your Producer Client behaves on scenarios like network errors or getting crappy data from the API you should use WireMock. 639 | 640 | #### Configure Mock Server 641 | 642 | Now let's define how a request from the **Consumer** looks like and what's the 643 | expected format of the payload. 644 | 645 | ``` javascript 1.6 646 | import { Matchers } from "@pact-foundation/pact"; 647 | 648 | beforeEach((done) => 649 | pact.setup() 650 | .then(() => { 651 | // define expected response 652 | const expectedResponse = { 653 | firstName: Matchers.like("aValidFirstName"), 654 | lastName: Matchers.like("aValidLastName"), 655 | age: Matchers.integer(100) 656 | }; 657 | 658 | // define request 659 | return pact.addInteraction({ 660 | state: "some user available", 661 | 662 | uponReceiving: "a user request", 663 | 664 | withRequest: { 665 | method: "GET", 666 | path: "/user", 667 | 668 | headers: { 669 | Accept: Matchers.term({ 670 | matcher: "application/json", 671 | generate: "application/json" 672 | }) 673 | }, 674 | }, 675 | 676 | willRespondWith: { 677 | status: 200, 678 | headers: {"Content-Type": "application/json;charset=UTF-8"}, 679 | body: expectedResponse 680 | } 681 | }); 682 | }) 683 | .then(() => done()) 684 | ); 685 | 686 | ``` 687 | 688 | ### Test 689 | At this point we should define our client-side test based on the defined request we 690 | described in the step before. So let our *UserClient* (that is talking to the **Provider**) 691 | call the mockServer (that was created for us by **Pact**) as we defined it in our consumer test 692 | and validate our expected response. 693 | 694 | ``` javascript 1.6 695 | it("can load user data", () => { 696 | 697 | expect.assertions(3); 698 | 699 | const url = `http://localhost:4711/user`; 700 | 701 | const promise = fetchUserData(url); 702 | 703 | return promise.then(response => { 704 | expect(response.status).toBe(200); 705 | expect(response.headers['content-type']).toBe("application/json;charset=UTF-8"); 706 | expect(response.data).toMatchObject({ 707 | firstName: "aValidFirstName", 708 | lastName: "aValidLastName", 709 | age: 100 710 | }); 711 | }); 712 | }); 713 | ``` 714 | 715 | #### create pact file 716 | Don't forget to create the Pact file(s) after a successful test run. 717 | 718 | ``` 719 | afterAll(() => { 720 | pact.finalize(); 721 | }); 722 | ``` 723 | 724 | ### Publish 725 | 726 | Now that we have successfully generated a Pact file (you'll find it in your build dir if everything went well) 727 | we want to upload it to the Pact Broker. Generally should be solved via a build step that should be executed when your tests are running on your CI system, 728 | but for demo purpose i'm going to upload it by calling `publishContract()` whenever the pact test succeeded. 729 | 730 | 731 | ``` javascript 1.6 732 | import { Publisher } from "@pact-foundation/pact-node"; 733 | 734 | const publishContract = () => { 735 | 736 | const options = { 737 | pactFilesOrDirs: [path.resolve(process.cwd(), "dist/pacts")], 738 | pactBroker: "http://localhost:80", 739 | consumerVersion: "0.1.0" 740 | }; 741 | 742 | new Publisher(options).publish(); 743 | }; 744 | ``` 745 | 746 | > ##### So your test class should look something like [THIS](consumer-ui/src/userApiContract.pact.test.js) 747 | 748 | Having a look at Pact-Broker UI you should see something like: 749 | ![pact uploaded](documentation/ui-uploaded-but-not-verified.png) 750 | 751 | ### make your Provider Test work 752 | When verifying a contract created by a Javascript consumer it is necessary to the following to your Producers Pact verification test. 753 | 754 | >using Kotlin: 755 | >``` kotlin 756 | >@State("some user available") 757 | >fun userAvailable() {} 758 | >``` 759 | 760 | >using Java: 761 | >``` java 762 | >@State("some user available") 763 | >public void userAvailable() {} 764 | >``` 765 | 766 | > ##### So your test class should look something like [THIS](producer/src/test/kotlin/com/example/demo/UserDataProviderContractIT.kt) if you are using Kotlin. 767 | > ##### So your test class should look something like [THIS](producer/src/test/kotlin/com/example/demo/JavaUserDataProviderContractIT.java) if you are using Java. 768 | 769 | Having a look at Pact-Broker UI you should see something like: 770 | ![pact uploaded](documentation/ui-uploaded-and-verified.png) 771 | 772 | When clicking on an arrow in the **Pact-Brokers** network graph overview you'll see a detailed view describing the exact properties 773 | a certain consumer relies on regarding the producers API. 774 | Which is really great from Producers point of view to know it's consumers and furthermore what data they are consuming in detail. 775 | 776 | ![broker-network-graph](documentation/broker-pact-details-consumer-ui.png) 777 | 778 | # _Spring Cloud Contract_ meets Pact 779 | 780 | So far we saw different possibilities on doing consumer driven contract testing using Pact. But on the one hand you don't always know 781 | your consumers (until they wrote a contract test) and on the other hand it's not nice to dictate a library or framework 782 | to your consumers. Assuming every application (including the maintainers) has to work with a certain tool doesn't sounds like a good idea regarding independent 783 | and autonomously working teams. Now the good news, [Spring-Cloud-Contract](https://spring.io/projects/spring-cloud-contract) has 784 | support for Pact by generating Pact-Files if wanted, which can of course be published to a Pact broker. 785 | 786 | ## The Consumer 787 | 788 | more coming soon ... 789 | 790 | ## The Provider 791 | 792 | First let's add the Spring Cloud Contract maven plugin and set it up to use our 793 | **Pact Broker** as its Contract repository. 794 | 795 | ``` xml 796 | 797 | org.springframework.cloud 798 | spring-cloud-contract-maven-plugin 799 | 2.0.2.RELEASE 800 | true 801 | 802 | pact://http://localhost:80 803 | 804 | 807 | 808 | ${project.groupId} 809 | ${project.artifactId} 810 | 811 | + 812 | 813 | 814 | 815 | REMOTE 816 | 817 | 818 | 819 | 820 | org.springframework.cloud 821 | spring-cloud-contract-pact 822 | 2.0.2.RELEASE 823 | 824 | 825 | 826 | ``` 827 | 828 | more coming soon ... 829 | 830 | # Messaging Example 831 | 832 | This is basically working the exact same way as our [REST-example](#rest-example-server-to-server-communication). 833 | How does that make sense? It's because of Pact is not going to start a 834 | Kafka, ActiveMQ or whatever mock. When talking about consumer driven contract tests with Pact and messaging 835 | it's about to make sure you can deserialize an events payload - describe how the messages have to look like for your consumer. 836 | 837 | **Let's start by defining our contract on the Consumer side again** 838 | 839 | #### prerequisites 840 | First let's add the relevant **Pact** dependency to the consumer applications *pom.xml*. 841 | 842 | ``` xml 843 | 844 | au.com.dius 845 | pact-jvm-consumer-java8_2.12 846 | 3.5.21 847 | test 848 | 849 | ``` 850 | 851 | ## The Consumer 852 | 853 | You'll find a working example in [MessagingContractTest](consumer/src/test/kotlin/com/example/demo/MessagingContractTest.kt) class. 854 | More documentation coming soon ... 855 | 856 | ## The Provider 857 | 858 | more coming soon ... 859 | 860 | ---------------- 861 | 862 | ### Extra infos on Pact 863 | #### [Terminology](https://docs.pact.io/terminology) 864 | 865 | ##### Service Consumer 866 | A component that initiates a HTTP request to another component 867 | (the service provider). Note that this does not depend on the way the 868 | data flows - whether it is a `GET` or a `PUT` / `POST` / `PATCH` / `DELETE`, the consumer is the initiator of the HTTP request. 869 | 870 | ##### Service Provider 871 | A server that responds to an HTTP request from another component 872 | (the service consumer). A service provider may have one or more HTTP endpoints, 873 | and should be thought of as the "deployable unit" - endpoints that get 874 | deployed together should be considered part of the same provider. 875 | 876 | ##### Mock Service Provider 877 | Used by tests in the consumer project to mock out the actual service provider, 878 | meaning that integration-like tests can be run without requiring the actual 879 | service provider to be available. 880 | 881 | ##### Interaction 882 | A request and response pair. A pact consists of a collection of interactions. 883 | 884 | ##### Pact file 885 | A file containing the JSON serialised interactions (requests and responses) 886 | that were defined in the consumer tests. This is the Contract. A Pact defines: 887 | 888 | * the consumer name 889 | * the provider name 890 | * a collection of interactions 891 | * the pact specification version (see below) 892 | 893 | ##### Pact verification 894 | To verify a Pact contract, the requests contained in a Pact file are replayed 895 | against the provider code, and the responses returned are checked to ensure 896 | they match those expected in the Pact file. 897 | 898 | ##### Provider state 899 | A name describing a “state” (like a fixture) that the provider should be 900 | in when a given request is replayed against it - e.g. “when user John Doe 901 | exists” or “when user John Doe has a bank account”. These allow the same 902 | endpoint to be tested under different scenarios. 903 | 904 | A provider state name is specified when writing the consumer specs, then, 905 | when the pact verification is set up in the provider the same name will be 906 | used to identify the set up code block that should be run before the request 907 | is executed. 908 | 909 | ##### Pact Specification 910 | The Pact Specification is a document that governs the structure of the actual 911 | generated Pact files to allow for interoperability between languages 912 | (consider, for example, a JavaScript consumer connecting to a Scala JVM-based 913 | provider) , using semantic versioning to indicate breaking changes. 914 | 915 | Each language implementation of Pact needs to implement the rules of this 916 | specification, and advertise which version(s) are supported, corresponding 917 | closely to which features are available. 918 | 919 | ## HELPFUL LINKS 920 | * [https://www.martinfowler.com/articles/consumerDrivenContracts.html](https://www.martinfowler.com/articles/consumerDrivenContracts.html) 921 | * [https://docs.pact.io](https://docs.pact.io) 922 | * [https://github.com/DiUS/pact-jvm](https://github.com/DiUS/pact-jvm) 923 | * [https://github.com/pact-foundation/pact_broker](https://github.com/pact-foundation/pact_broker) 924 | * [https://github.com/pact-foundation/pact-js](https://github.com/pact-foundation/pact-js) 925 | * [https://www.schibsted.pl/blog/contract-testing](https://www.schibsted.pl/blog/contract-testing/) 926 | * [https://devblog.xero.com/trust-but-verify-using-pact-for-contract-testing-495a1e303a6](https://devblog.xero.com/trust-but-verify-using-pact-for-contract-testing-495a1e303a6) 927 | * [https://reflectoring.io/7-reasons-for-consumer-driven-contracts](https://reflectoring.io/7-reasons-for-consumer-driven-contracts/) 928 | * [https://www.slideshare.net/paucls/consumerdriven-contract-testing](https://www.slideshare.net/paucls/consumerdriven-contract-testing) 929 | * [https://medium.com/techbeatscorner/consumer-driven-contracts-with-pact-jvm-and-groovy-e329196e4dd](https://medium.com/techbeatscorner/consumer-driven-contracts-with-pact-jvm-and-groovy-e329196e4dd) 930 | * [https://www.baeldung.com/pact-junit-consumer-driven-contracts](https://www.baeldung.com/pact-junit-consumer-driven-contracts) 931 | * [https://kreuzwerker.de/blog/writing-contract-tests-with-pact-in-spring-boot](https://kreuzwerker.de/blog/writing-contract-tests-with-pact-in-spring-boot) 932 | * [https://blog.shanelee.name/2016/07/19/consumer-driven-contract-testing-using-pact/](https://blog.shanelee.name/2016/07/19/consumer-driven-contract-testing-using-pact/) 933 | * [https://ordina-jworks.github.io/spring/2018/04/28/Spring-Cloud-Contract-meet-Pact.html](https://ordina-jworks.github.io/spring/2018/04/28/Spring-Cloud-Contract-meet-Pact.html) 934 | * [https://reflectoring.io/consumer-driven-contract-provider-spring-cloud-contract](https://reflectoring.io/consumer-driven-contract-provider-spring-cloud-contract) 935 | * [https://cloud.spring.io/spring-cloud-contract/single/spring-cloud-contract.html#_can_i_use_the_pact_broker](https://cloud.spring.io/spring-cloud-contract/single/spring-cloud-contract.html#_can_i_use_the_pact_broker) 936 | * [https://cloud.spring.io/spring-cloud-contract/single/spring-cloud-contract.html#pact-converter](https://cloud.spring.io/spring-cloud-contract/single/spring-cloud-contract.html#pact-converter) 937 | * [https://www.youtube.com/watch?v=sAAklvxmPmk](https://www.youtube.com/watch?v=sAAklvxmPmk) 938 | * [http://blog.pact.io/2018/07/24/contract-testing-a-graphql-api](http://blog.pact.io/2018/07/24/contract-testing-a-graphql-api/) 939 | --------------------------------------------------------------------------------