├── order-consumer ├── src │ ├── main │ │ └── java │ │ │ └── order │ │ │ ├── NotFoundException.java │ │ │ ├── Address.java │ │ │ ├── AddressServiceClient.java │ │ │ └── AddressErrorHandler.java │ └── test │ │ └── java │ │ └── order │ │ ├── AddressId.java │ │ ├── RandomPort.java │ │ ├── AddressServiceDeleteContractTest.java │ │ └── AddressServiceGetContractTest.java └── pom.xml ├── address-provider ├── src │ ├── main │ │ ├── java │ │ │ └── provider │ │ │ │ ├── NotFoundException.java │ │ │ │ ├── BadRequestException.java │ │ │ │ ├── AddressServiceApplication.java │ │ │ │ ├── Address.java │ │ │ │ ├── GlobalControllerExceptionHandler.java │ │ │ │ ├── AddressService.java │ │ │ │ └── AddressController.java │ │ └── resources │ │ │ └── application.yml │ └── test │ │ ├── java │ │ └── provider │ │ │ └── ContractVerificationTest.java │ │ └── pacts │ │ └── order_consumer-address_provider.json └── pom.xml ├── customer-consumer ├── src │ ├── main │ │ └── java │ │ │ └── customer │ │ │ ├── NotFoundException.java │ │ │ ├── Address.java │ │ │ ├── AddressServiceClient.java │ │ │ └── AddressErrorHandler.java │ └── test │ │ └── java │ │ └── customer │ │ ├── AddressId.java │ │ ├── RandomPort.java │ │ ├── AddressServiceDeleteContractTest.java │ │ └── AddressServiceGetContractTest.java └── pom.xml ├── README.md ├── pom.xml └── .gitignore /order-consumer/src/main/java/order/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package order; 2 | 3 | public class NotFoundException extends RuntimeException { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /address-provider/src/main/java/provider/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package provider; 2 | 3 | public class NotFoundException extends RuntimeException { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /customer-consumer/src/main/java/customer/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package customer; 2 | 3 | public class NotFoundException extends RuntimeException { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /address-provider/src/main/java/provider/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package provider; 2 | 3 | public class BadRequestException extends RuntimeException { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /address-provider/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8090 3 | 4 | spring: 5 | jackson: 6 | serialization: 7 | write-dates-as-timestamps: false 8 | date-format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z' -------------------------------------------------------------------------------- /order-consumer/src/test/java/order/AddressId.java: -------------------------------------------------------------------------------- 1 | package order; 2 | 3 | public class AddressId { 4 | 5 | public static final String EXISTING_ADDRESS_ID = "8aed8fad-d554-4af8-abf5-a65830b49a5f"; 6 | public static final String NON_EXISTING_ADDRESS_ID = "00000000-0000-0000-0000-000000000000"; 7 | } 8 | -------------------------------------------------------------------------------- /customer-consumer/src/test/java/customer/AddressId.java: -------------------------------------------------------------------------------- 1 | package customer; 2 | 3 | public class AddressId { 4 | 5 | public static final String EXISTING_ADDRESS_ID = "8aed8fad-d554-4af8-abf5-a65830b49a5f"; 6 | public static final String NON_EXISTING_ADDRESS_ID = "00000000-0000-0000-0000-000000000000"; 7 | } 8 | -------------------------------------------------------------------------------- /address-provider/src/main/java/provider/AddressServiceApplication.java: -------------------------------------------------------------------------------- 1 | package provider; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class AddressServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(AddressServiceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /customer-consumer/src/main/java/customer/Address.java: -------------------------------------------------------------------------------- 1 | package customer; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | @Data @NoArgsConstructor 7 | public class Address { 8 | 9 | private String id; 10 | private String addressType; 11 | private String street; 12 | private int number; 13 | private String city; 14 | private int zipCode; 15 | private String state; 16 | private String country; 17 | } 18 | -------------------------------------------------------------------------------- /order-consumer/src/main/java/order/Address.java: -------------------------------------------------------------------------------- 1 | package order; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.util.UUID; 7 | 8 | @Data @NoArgsConstructor 9 | public class Address { 10 | 11 | private String id; 12 | private String addressType; 13 | private String street; 14 | private int number; 15 | private String city; 16 | private int zipCode; 17 | private String state; 18 | private String country; 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction to contract testing 2 | 3 | This is the codebase associated with the [Introduction to contract testing workshop](https://www.ontestautomation.com/training/contract-testing/) I've been running successfully for a number of years now. 4 | 5 | ### Prerequisites 6 | * Java 17 7 | * Maven 8 | * A free [PactFlow](https://www.pactflow.io) account 9 | 10 | ### Interested in this workshop? 11 | Send me an email at bas@ontestautomation.com and I'm happy to discuss options. -------------------------------------------------------------------------------- /address-provider/src/main/java/provider/Address.java: -------------------------------------------------------------------------------- 1 | package provider; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.util.UUID; 7 | 8 | @Data @NoArgsConstructor 9 | public class Address { 10 | 11 | private UUID id; 12 | private String addressType; 13 | private String street; 14 | private int number; 15 | private String city; 16 | private int zipCode; 17 | private String state; 18 | private String country; 19 | } 20 | -------------------------------------------------------------------------------- /address-provider/src/main/java/provider/GlobalControllerExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package provider; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ControllerAdvice; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | import org.springframework.web.bind.annotation.ResponseStatus; 7 | 8 | @ControllerAdvice 9 | public class GlobalControllerExceptionHandler { 10 | 11 | @ExceptionHandler(NotFoundException.class) 12 | @ResponseStatus(HttpStatus.NOT_FOUND) 13 | public void handleNotFound() { 14 | } 15 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | com.ontestautomation.cdct 7 | introduction-to-contract-testing 8 | pom 9 | 1.0-SNAPSHOT 10 | Examples used in the Introduction to Contract Testing blog post series 11 | 12 | customer-consumer 13 | order-consumer 14 | address-provider 15 | 16 | -------------------------------------------------------------------------------- /order-consumer/src/test/java/order/RandomPort.java: -------------------------------------------------------------------------------- 1 | package order; 2 | 3 | import java.io.IOException; 4 | import java.net.ServerSocket; 5 | 6 | public class RandomPort { 7 | 8 | private static RandomPort INSTANCE; 9 | 10 | private int port; 11 | 12 | private RandomPort() { 13 | try (ServerSocket serverSocket = new ServerSocket(0)) { 14 | this.port = serverSocket.getLocalPort(); 15 | System.setProperty("RANDOM_PORT", String.valueOf(this.port)); 16 | } 17 | catch (IOException ignored) { 18 | } 19 | } 20 | 21 | public static RandomPort getInstance() { 22 | if (INSTANCE == null) { 23 | INSTANCE = new RandomPort(); 24 | } 25 | return INSTANCE; 26 | } 27 | 28 | public int getPort() { 29 | return this.port; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /customer-consumer/src/test/java/customer/RandomPort.java: -------------------------------------------------------------------------------- 1 | package customer; 2 | 3 | import java.io.IOException; 4 | import java.net.ServerSocket; 5 | 6 | public class RandomPort { 7 | 8 | private static RandomPort INSTANCE; 9 | 10 | private int port; 11 | 12 | private RandomPort() { 13 | try (ServerSocket serverSocket = new ServerSocket(0)) { 14 | this.port = serverSocket.getLocalPort(); 15 | System.setProperty("RANDOM_PORT", String.valueOf(this.port)); 16 | } 17 | catch (IOException ignored) { 18 | } 19 | } 20 | 21 | public static RandomPort getInstance() { 22 | if (INSTANCE == null) { 23 | INSTANCE = new RandomPort(); 24 | } 25 | return INSTANCE; 26 | } 27 | 28 | public int getPort() { 29 | return this.port; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /address-provider/src/main/java/provider/AddressService.java: -------------------------------------------------------------------------------- 1 | package provider; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.UUID; 8 | 9 | @Service 10 | public class AddressService { 11 | 12 | public Address getAddress(String addressId) { 13 | 14 | Address address = new Address(); 15 | 16 | address.setId(UUID.fromString(addressId)); 17 | address.setAddressType("billing"); 18 | address.setStreet("Main Street"); 19 | address.setNumber(123); 20 | address.setCity("Nothingville"); 21 | address.setZipCode(54321); 22 | address.setState("Tennessee"); 23 | address.setCountry("United States"); 24 | 25 | return address; 26 | } 27 | 28 | public void deleteAddress(String addressId) { 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /address-provider/src/main/java/provider/AddressController.java: -------------------------------------------------------------------------------- 1 | package provider; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.*; 5 | 6 | @RestController 7 | public class AddressController { 8 | 9 | private final AddressService addressService; 10 | 11 | public AddressController(AddressService addressService) { 12 | this.addressService = addressService; 13 | } 14 | 15 | @GetMapping("/address/{addressId}") 16 | public Address getAddress(@PathVariable String addressId) { 17 | 18 | if(addressId.equalsIgnoreCase("00000000-0000-0000-0000-000000000000")) { 19 | throw new NotFoundException(); 20 | } 21 | 22 | return addressService.getAddress(addressId); 23 | } 24 | 25 | @DeleteMapping("/address/{addressId}") 26 | @ResponseStatus(value = HttpStatus.NO_CONTENT) 27 | public void deleteAddress(@PathVariable String addressId) { 28 | 29 | addressService.deleteAddress(addressId); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /customer-consumer/src/main/java/customer/AddressServiceClient.java: -------------------------------------------------------------------------------- 1 | package customer; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.boot.web.client.RestTemplateBuilder; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.client.RestTemplate; 7 | 8 | @Component 9 | public class AddressServiceClient { 10 | 11 | private final RestTemplate restTemplate; 12 | 13 | public AddressServiceClient(@Value("${address_provider.base-url}") String baseUrl) { 14 | this.restTemplate = new RestTemplateBuilder() 15 | .errorHandler(new AddressErrorHandler()) 16 | .rootUri(baseUrl) 17 | .defaultHeader("Connection", "close") 18 | .build(); 19 | } 20 | 21 | public Address getAddress(String addressId) { 22 | return restTemplate.getForObject(String.format("/address/%s", addressId), Address.class); 23 | } 24 | 25 | public void deleteAddress(String addressId) { 26 | restTemplate.delete(String.format("/address/%s", addressId)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /order-consumer/src/main/java/order/AddressServiceClient.java: -------------------------------------------------------------------------------- 1 | package order; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.boot.web.client.RestTemplateBuilder; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.client.RestTemplate; 8 | 9 | @Component 10 | public class AddressServiceClient { 11 | 12 | private final RestTemplate restTemplate; 13 | 14 | public AddressServiceClient(@Value("${address_provider.base-url}") String baseUrl) { 15 | this.restTemplate = new RestTemplateBuilder() 16 | .errorHandler(new AddressErrorHandler()) 17 | .rootUri(baseUrl) 18 | .defaultHeader("Connection", "close") 19 | .build(); 20 | } 21 | 22 | public Address getAddress(String addressId) { 23 | return restTemplate.getForObject(String.format("/address/%s", addressId), Address.class); 24 | } 25 | 26 | public void deleteAddress(String addressId) { 27 | restTemplate.delete(String.format("/address/%s", addressId)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | 11 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 12 | !/.mvn/wrapper/maven-wrapper.jar 13 | 14 | 15 | .metadata 16 | bin/ 17 | tmp/ 18 | *.tmp 19 | *.bak 20 | *.swp 21 | *~.nib 22 | local.properties 23 | .settings/ 24 | .loadpath 25 | .recommenders 26 | .classpath 27 | .project 28 | 29 | # TestNG reporting 30 | test-output/ 31 | 32 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 33 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 34 | 35 | .idea/ 36 | 37 | # CMake 38 | cmake-build-debug/ 39 | 40 | ## File-based project format: 41 | *.iws 42 | 43 | *.iml 44 | 45 | ## Plugin-specific files: 46 | 47 | # IntelliJ 48 | out/ 49 | 50 | # mpeltonen/sbt-idea plugin 51 | .idea_modules/ 52 | 53 | # JIRA plugin 54 | atlassian-ide-plugin.xml 55 | 56 | # Cursive Clojure plugin 57 | .idea/replstate.xml 58 | 59 | # Crashlytics plugin (for Android Studio and IntelliJ) 60 | com_crashlytics_export_strings.xml 61 | crashlytics.properties 62 | crashlytics-build.properties 63 | fabric.properties -------------------------------------------------------------------------------- /order-consumer/src/main/java/order/AddressErrorHandler.java: -------------------------------------------------------------------------------- 1 | package order; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.client.ClientHttpResponse; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.client.HttpClientErrorException; 7 | import org.springframework.web.client.ResponseErrorHandler; 8 | 9 | import java.io.IOException; 10 | 11 | @Component 12 | public class AddressErrorHandler implements ResponseErrorHandler { 13 | 14 | @Override 15 | public boolean hasError(ClientHttpResponse httpResponse) throws IOException { 16 | return httpResponse.getStatusCode().is5xxServerError() || httpResponse.getStatusCode().is4xxClientError(); 17 | } 18 | 19 | @Override 20 | public void handleError(ClientHttpResponse httpResponse) throws IOException { 21 | if (httpResponse.getStatusCode().is5xxServerError()) { 22 | //Handle SERVER_ERROR 23 | throw new HttpClientErrorException(httpResponse.getStatusCode()); 24 | } else if (httpResponse.getStatusCode().is4xxClientError()) { 25 | //Handle CLIENT_ERROR 26 | if (httpResponse.getStatusCode() == HttpStatus.NOT_FOUND) { 27 | throw new NotFoundException(); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /customer-consumer/src/main/java/customer/AddressErrorHandler.java: -------------------------------------------------------------------------------- 1 | package customer; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.client.ClientHttpResponse; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.client.HttpClientErrorException; 7 | import org.springframework.web.client.ResponseErrorHandler; 8 | 9 | import java.io.IOException; 10 | 11 | @Component 12 | public class AddressErrorHandler implements ResponseErrorHandler { 13 | 14 | @Override 15 | public boolean hasError(ClientHttpResponse httpResponse) throws IOException { 16 | return httpResponse.getStatusCode().is5xxServerError() || httpResponse.getStatusCode().is4xxClientError(); 17 | } 18 | 19 | @Override 20 | public void handleError(ClientHttpResponse httpResponse) throws IOException { 21 | if (httpResponse.getStatusCode().is5xxServerError()) { 22 | //Handle SERVER_ERROR 23 | throw new HttpClientErrorException(httpResponse.getStatusCode()); 24 | } else if (httpResponse.getStatusCode().is4xxClientError()) { 25 | //Handle CLIENT_ERROR 26 | if (httpResponse.getStatusCode() == HttpStatus.NOT_FOUND) { 27 | throw new NotFoundException(); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /order-consumer/src/test/java/order/AddressServiceDeleteContractTest.java: -------------------------------------------------------------------------------- 1 | package order; 2 | 3 | import au.com.dius.pact.consumer.MockServer; 4 | import au.com.dius.pact.consumer.dsl.PactDslWithProvider; 5 | import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; 6 | import au.com.dius.pact.consumer.junit5.PactTestFor; 7 | import au.com.dius.pact.core.model.PactSpecVersion; 8 | import au.com.dius.pact.core.model.RequestResponsePact; 9 | import au.com.dius.pact.core.model.annotations.Pact; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import java.io.IOException; 13 | 14 | @ExtendWith(PactConsumerTestExt.class) 15 | @PactTestFor(providerName = "address_provider", pactVersion = PactSpecVersion.V3) 16 | public class AddressServiceDeleteContractTest { 17 | 18 | @Pact(provider = "address_provider", consumer = "order_consumer") 19 | public RequestResponsePact pactForDeleteCorrectlyFormattedAddressId(PactDslWithProvider builder) { 20 | 21 | return builder 22 | .given("No specific state required") 23 | .uponReceiving("Deleting an address ID") 24 | .path(String.format("/address/%s", AddressId.EXISTING_ADDRESS_ID)) 25 | .method("DELETE") 26 | .willRespondWith() 27 | .status(204) 28 | .toPact(); 29 | } 30 | 31 | @Test 32 | @PactTestFor(pactMethod = "pactForDeleteCorrectlyFormattedAddressId") 33 | public void testFor_DELETE_correctlyFormattedAddressId_shouldYieldHttp204(MockServer mockServer) throws IOException { 34 | 35 | AddressServiceClient client = new AddressServiceClient(mockServer.getUrl()); 36 | 37 | client.deleteAddress(AddressId.EXISTING_ADDRESS_ID); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /customer-consumer/src/test/java/customer/AddressServiceDeleteContractTest.java: -------------------------------------------------------------------------------- 1 | package customer; 2 | 3 | import au.com.dius.pact.consumer.MockServer; 4 | import au.com.dius.pact.consumer.dsl.PactDslWithProvider; 5 | import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; 6 | import au.com.dius.pact.consumer.junit5.PactTestFor; 7 | import au.com.dius.pact.core.model.PactSpecVersion; 8 | import au.com.dius.pact.core.model.RequestResponsePact; 9 | import au.com.dius.pact.core.model.annotations.Pact; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import java.io.IOException; 13 | 14 | @ExtendWith(PactConsumerTestExt.class) 15 | @PactTestFor(providerName = "address_provider", pactVersion = PactSpecVersion.V3) 16 | public class AddressServiceDeleteContractTest { 17 | 18 | @Pact(provider = "address_provider", consumer = "customer_consumer") 19 | public RequestResponsePact pactForDeleteCorrectlyFormattedAddressId(PactDslWithProvider builder) { 20 | 21 | return builder.given( 22 | "No specific state required") 23 | .uponReceiving("Deleting a valid address ID") 24 | .path(String.format("/address/%s", AddressId.EXISTING_ADDRESS_ID)) 25 | .method("DELETE") 26 | .willRespondWith() 27 | .status(204) 28 | .toPact(); 29 | } 30 | 31 | @Test 32 | @PactTestFor(pactMethod = "pactForDeleteCorrectlyFormattedAddressId") 33 | public void testFor_DELETE_correctlyFormattedAddressId_shouldYieldHttp204(MockServer mockServer) throws IOException { 34 | 35 | AddressServiceClient client = new AddressServiceClient(mockServer.getUrl()); 36 | 37 | client.deleteAddress(AddressId.EXISTING_ADDRESS_ID); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /address-provider/src/test/java/provider/ContractVerificationTest.java: -------------------------------------------------------------------------------- 1 | package provider; 2 | 3 | import au.com.dius.pact.provider.junit5.HttpTestTarget; 4 | import au.com.dius.pact.provider.junit5.PactVerificationContext; 5 | import au.com.dius.pact.provider.junitsupport.Provider; 6 | import au.com.dius.pact.provider.junitsupport.State; 7 | import au.com.dius.pact.provider.junitsupport.loader.PactBroker; 8 | import au.com.dius.pact.provider.junitsupport.loader.PactBrokerAuth; 9 | import au.com.dius.pact.provider.junitsupport.loader.PactFolder; 10 | import au.com.dius.pact.provider.spring.junit5.PactVerificationSpringProvider; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.TestTemplate; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.boot.test.web.server.LocalServerPort; 16 | import org.springframework.test.context.junit.jupiter.SpringExtension; 17 | 18 | import java.util.Map; 19 | 20 | @ExtendWith(SpringExtension.class) 21 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 22 | @Provider("address_provider") 23 | //@PactFolder("src/test/pacts") 24 | @PactBroker(url="${PACT_BROKER_BASE_URL}", authentication = @PactBrokerAuth(token = "${PACT_BROKER_TOKEN}")) 25 | public class ContractVerificationTest { 26 | 27 | @LocalServerPort 28 | int port; 29 | 30 | @BeforeEach 31 | public void setUp(PactVerificationContext context) { 32 | context.setTarget(new HttpTestTarget("localhost", port)); 33 | } 34 | 35 | @TestTemplate 36 | @ExtendWith(PactVerificationSpringProvider.class) 37 | public void pactVerificationTestTemplate(PactVerificationContext context) { 38 | context.verifyInteraction(); 39 | } 40 | 41 | @State("Address exists") 42 | public void addressWithIdExists(Map providerStateParams) { 43 | } 44 | 45 | @State("Address does not exist") 46 | public void addressWithIdDoesNotExist(Map providerStateParams) { 47 | } 48 | 49 | @State("No specific state required") 50 | public void noSpecificStateRequired() { 51 | } 52 | } -------------------------------------------------------------------------------- /address-provider/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.ontestautomation.cdct 8 | address-provider 9 | 1.0.0 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 3.1.3 15 | 16 | 17 | 18 | 19 | 21 20 | 21 21 | UTF-8 22 | UTF-8 23 | 21 24 | 5.9.2 25 | 4.6.14 26 | 1.18.34 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-web 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-test 38 | test 39 | 40 | 41 | 42 | org.junit.jupiter 43 | junit-jupiter-engine 44 | ${junit.version} 45 | test 46 | 47 | 48 | 49 | au.com.dius.pact 50 | provider 51 | ${pact.version} 52 | test 53 | 54 | 55 | 56 | au.com.dius.pact.provider 57 | junit5spring 58 | ${pact.version} 59 | test 60 | 61 | 62 | 63 | org.projectlombok 64 | lombok 65 | ${lombok.version} 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /order-consumer/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.ontestautomation.cdct 8 | order-consumer 9 | 1.0.0 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 3.3.4 15 | 16 | 17 | 18 | 19 | 21 20 | 21 21 | UTF-8 22 | UTF-8 23 | 5.10.0 24 | 4.6.14 25 | 4.0.10 26 | 1.18.34 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-web 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-test 38 | test 39 | 40 | 41 | 42 | org.junit.jupiter 43 | junit-jupiter-engine 44 | ${junit.version} 45 | test 46 | 47 | 48 | 49 | au.com.dius.pact 50 | consumer 51 | ${pact.version} 52 | test 53 | 54 | 55 | 56 | au.com.dius.pact.consumer 57 | junit5 58 | ${pact.version} 59 | test 60 | 61 | 62 | 63 | au.com.dius 64 | pact-jvm-consumer-junit5 65 | ${pact.consumer.junit5.version} 66 | test 67 | 68 | 69 | 70 | org.projectlombok 71 | lombok 72 | ${lombok.version} 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | au.com.dius.pact.provider 81 | maven 82 | ${pact.version} 83 | 84 | ${env.PACT_BROKER_BASE_URL} 85 | ${env.PACT_BROKER_TOKEN} 86 | Bearer 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /customer-consumer/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.ontestautomation.cdct 8 | customer-consumer 9 | 1.0.0 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 3.3.4 15 | 16 | 17 | 18 | 19 | 21 20 | 21 21 | UTF-8 22 | UTF-8 23 | 5.10.0 24 | 4.6.14 25 | 4.0.10 26 | 1.18.34 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-web 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-test 38 | test 39 | 40 | 41 | 42 | org.junit.jupiter 43 | junit-jupiter-engine 44 | ${junit.version} 45 | test 46 | 47 | 48 | 49 | au.com.dius.pact 50 | consumer 51 | ${pact.version} 52 | test 53 | 54 | 55 | 56 | au.com.dius.pact.consumer 57 | junit5 58 | ${pact.version} 59 | test 60 | 61 | 62 | 63 | au.com.dius 64 | pact-jvm-consumer-junit5 65 | ${pact.consumer.junit5.version} 66 | test 67 | 68 | 69 | 70 | org.projectlombok 71 | lombok 72 | ${lombok.version} 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | au.com.dius.pact.provider 81 | maven 82 | ${pact.version} 83 | 84 | ${env.PACT_BROKER_BASE_URL} 85 | ${env.PACT_BROKER_TOKEN} 86 | Bearer 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /order-consumer/src/test/java/order/AddressServiceGetContractTest.java: -------------------------------------------------------------------------------- 1 | package order; 2 | 3 | import au.com.dius.pact.consumer.MockServer; 4 | import au.com.dius.pact.consumer.dsl.DslPart; 5 | import au.com.dius.pact.consumer.dsl.LambdaDsl; 6 | import au.com.dius.pact.consumer.dsl.PactDslWithProvider; 7 | import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; 8 | import au.com.dius.pact.consumer.junit5.PactTestFor; 9 | import au.com.dius.pact.core.model.PactSpecVersion; 10 | import au.com.dius.pact.core.model.RequestResponsePact; 11 | import au.com.dius.pact.core.model.annotations.Pact; 12 | import org.junit.jupiter.api.Assertions; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | 16 | import java.util.Map; 17 | import java.util.UUID; 18 | 19 | @ExtendWith(PactConsumerTestExt.class) 20 | @PactTestFor(providerName = "address_provider", pactVersion = PactSpecVersion.V3) 21 | public class AddressServiceGetContractTest { 22 | 23 | @Pact(provider = "address_provider", consumer = "order_consumer") 24 | public RequestResponsePact pactForGetExistingAddressId(PactDslWithProvider builder) { 25 | 26 | DslPart body = LambdaDsl.newJsonBody((o) -> o 27 | .uuid("id", UUID.fromString(AddressId.EXISTING_ADDRESS_ID)) 28 | .stringType("addressType", "billing") 29 | .stringType("street", "Main Street") 30 | .integerType("number", 123) 31 | .stringType("city", "Nothingville") 32 | .integerType("zipCode", 54321) 33 | .stringType("state", "Tennessee") 34 | .stringMatcher("country", "United States|Canada", "United States") 35 | ).build(); 36 | 37 | Map providerStateParams = Map.of("addressId", AddressId.EXISTING_ADDRESS_ID); 38 | 39 | return builder.given("Address exists", providerStateParams) 40 | .uponReceiving("Retrieving an existing address ID") 41 | .path(String.format("/address/%s", AddressId.EXISTING_ADDRESS_ID)) 42 | .method("GET") 43 | .willRespondWith() 44 | .status(200) 45 | .body(body) 46 | .toPact(); 47 | } 48 | 49 | @Pact(provider = "address_provider", consumer = "order_consumer") 50 | public RequestResponsePact pactForGetNonExistentAddressId(PactDslWithProvider builder) { 51 | 52 | Map providerStateParams = Map.of("addressId", AddressId.NON_EXISTING_ADDRESS_ID); 53 | 54 | return builder 55 | .given("Address does not exist", providerStateParams) 56 | .uponReceiving("Retrieving an address ID that does not exist") 57 | .path(String.format("/address/%s", AddressId.NON_EXISTING_ADDRESS_ID)) 58 | .method("GET") 59 | .willRespondWith() 60 | .status(404) 61 | .toPact(); 62 | } 63 | 64 | @Test 65 | @PactTestFor(pactMethod = "pactForGetExistingAddressId") 66 | public void testFor_GET_existingAddressId_shouldYieldExpectedAddressData(MockServer mockServer) { 67 | 68 | AddressServiceClient client = new AddressServiceClient(mockServer.getUrl()); 69 | 70 | Address address = client.getAddress(AddressId.EXISTING_ADDRESS_ID); 71 | 72 | Assertions.assertEquals(AddressId.EXISTING_ADDRESS_ID, address.getId()); 73 | } 74 | 75 | @Test 76 | @PactTestFor(pactMethod = "pactForGetNonExistentAddressId") 77 | public void testFor_GET_nonExistentAddressId_shouldYieldHttp404(MockServer mockServer) { 78 | 79 | AddressServiceClient client = new AddressServiceClient(mockServer.getUrl()); 80 | 81 | Assertions.assertThrows(NotFoundException.class, () -> client.getAddress(AddressId.NON_EXISTING_ADDRESS_ID)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /address-provider/src/test/pacts/order_consumer-address_provider.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumer": { 3 | "name": "order_consumer" 4 | }, 5 | "interactions": [ 6 | { 7 | "description": "Retrieving an address ID that does not exist", 8 | "providerStates": [ 9 | { 10 | "name": "Address with ID 00000000-0000-0000-0000-000000000000 does not exist" 11 | } 12 | ], 13 | "request": { 14 | "method": "GET", 15 | "path": "/address/00000000-0000-0000-0000-000000000000" 16 | }, 17 | "response": { 18 | "status": 404 19 | } 20 | }, 21 | { 22 | "description": "Retrieving an existing address ID", 23 | "providerStates": [ 24 | { 25 | "name": "Address with ID 8aed8fad-d554-4af8-abf5-a65830b49a5f exists" 26 | } 27 | ], 28 | "request": { 29 | "method": "GET", 30 | "path": "/address/8aed8fad-d554-4af8-abf5-a65830b49a5f" 31 | }, 32 | "response": { 33 | "body": { 34 | "addressType": "billing", 35 | "city": "Nothingville", 36 | "country": "United States", 37 | "id": "8aed8fad-d554-4af8-abf5-a65830b49a5f", 38 | "number": 123, 39 | "state": "Tennessee", 40 | "street": "Main Street", 41 | "zipCode": 54321 42 | }, 43 | "headers": { 44 | "Content-Type": "application/json; charset=UTF-8" 45 | }, 46 | "matchingRules": { 47 | "body": { 48 | "$.addressType": { 49 | "combine": "AND", 50 | "matchers": [ 51 | { 52 | "match": "type" 53 | } 54 | ] 55 | }, 56 | "$.city": { 57 | "combine": "AND", 58 | "matchers": [ 59 | { 60 | "match": "type" 61 | } 62 | ] 63 | }, 64 | "$.country": { 65 | "combine": "AND", 66 | "matchers": [ 67 | { 68 | "match": "regex", 69 | "regex": "United States|Canada" 70 | } 71 | ] 72 | }, 73 | "$.id": { 74 | "combine": "AND", 75 | "matchers": [ 76 | { 77 | "match": "regex", 78 | "regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" 79 | } 80 | ] 81 | }, 82 | "$.number": { 83 | "combine": "AND", 84 | "matchers": [ 85 | { 86 | "match": "integer" 87 | } 88 | ] 89 | }, 90 | "$.state": { 91 | "combine": "AND", 92 | "matchers": [ 93 | { 94 | "match": "type" 95 | } 96 | ] 97 | }, 98 | "$.street": { 99 | "combine": "AND", 100 | "matchers": [ 101 | { 102 | "match": "type" 103 | } 104 | ] 105 | }, 106 | "$.zipCode": { 107 | "combine": "AND", 108 | "matchers": [ 109 | { 110 | "match": "integer" 111 | } 112 | ] 113 | } 114 | }, 115 | "header": { 116 | "Content-Type": { 117 | "combine": "AND", 118 | "matchers": [ 119 | { 120 | "match": "regex", 121 | "regex": "application/json(;\\s?charset=[\\w\\-]+)?" 122 | } 123 | ] 124 | } 125 | } 126 | }, 127 | "status": 200 128 | } 129 | }, 130 | { 131 | "description": "Deleting an address ID", 132 | "providerStates": [ 133 | { 134 | "name": "No specific state required" 135 | } 136 | ], 137 | "request": { 138 | "method": "DELETE", 139 | "path": "/address/8aed8fad-d554-4af8-abf5-a65830b49a5f" 140 | }, 141 | "response": { 142 | "status": 204 143 | } 144 | } 145 | ], 146 | "metadata": { 147 | "pact-jvm": { 148 | "version": "4.6.14" 149 | }, 150 | "pactSpecification": { 151 | "version": "3.0.0" 152 | } 153 | }, 154 | "provider": { 155 | "name": "address_provider" 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /customer-consumer/src/test/java/customer/AddressServiceGetContractTest.java: -------------------------------------------------------------------------------- 1 | package customer; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.core.IsEqual.equalTo; 6 | 7 | import au.com.dius.pact.consumer.MockServer; 8 | import au.com.dius.pact.consumer.dsl.DslPart; 9 | import au.com.dius.pact.consumer.dsl.LambdaDsl; 10 | import au.com.dius.pact.consumer.dsl.PactDslWithProvider; 11 | import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; 12 | import au.com.dius.pact.consumer.junit5.PactTestFor; 13 | import au.com.dius.pact.core.model.PactSpecVersion; 14 | import au.com.dius.pact.core.model.RequestResponsePact; 15 | import au.com.dius.pact.core.model.annotations.Pact; 16 | import org.apache.http.HttpResponse; 17 | import org.apache.http.client.fluent.Request; 18 | import org.junit.jupiter.api.Assertions; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.extension.ExtendWith; 21 | 22 | import java.io.IOException; 23 | import java.util.UUID; 24 | 25 | @ExtendWith(PactConsumerTestExt.class) 26 | @PactTestFor(providerName = "address_provider", pactVersion = PactSpecVersion.V3) 27 | public class AddressServiceGetContractTest { 28 | 29 | @Pact(provider = "address_provider", consumer = "customer_consumer") 30 | public RequestResponsePact pactForGetExistingAddressId(PactDslWithProvider builder) { 31 | 32 | /** 33 | * TODO: Add the following expectations for the provider response to the existing ones: 34 | * - The response should contain a field 'zipCode' with an integer value 35 | * - The response should contain a field 'state' with a string value 36 | * - EXTRA: The response should contain a field 'country' with a value that can only be 'United States' or 'Canada' 37 | * (for the last one, have a look at https://docs.pact.io/implementation_guides/jvm/consumer#dsl-matching-methods for a hint) 38 | */ 39 | DslPart body = LambdaDsl.newJsonBody((o) -> o 40 | .uuid("id", UUID.fromString(AddressId.EXISTING_ADDRESS_ID)) 41 | .stringType("addressType", "billing") 42 | .stringType("street", "Main Street") 43 | .integerType("number", 123) 44 | .stringType("city", "Nothingville") 45 | ).build(); 46 | 47 | return builder.given( 48 | String.format("Address with ID %s exists", AddressId.EXISTING_ADDRESS_ID)) 49 | .uponReceiving("Retrieving an existing address ID") 50 | .path(String.format("/address/%s", AddressId.EXISTING_ADDRESS_ID)) 51 | .method("GET") 52 | .willRespondWith() 53 | .status(200) 54 | .body(body) 55 | .toPact(); 56 | } 57 | 58 | /** 59 | * TODO: uncomment the pactForGetNonExistentAddressId() method and complete its implementation by: 60 | * - removing the 'return null;' statement from the code 61 | * - specifying that a GET to /address/00000000-0000-0000-0000-000000000000 is to be performed 62 | * - specifying that this request should return an HTTP 404 63 | * - generating a pact segment from these expectations and returning that 64 | * You should use a provider state with the exact name 'Address with ID 00000000-0000-0000-0000-000000000000 does not exist' 65 | * The implementation is very similar to the one above, but does not need the body() part as we don't expect 66 | * the provider to return a response body in this situation. 67 | */ 68 | // @Pact(provider = "address_provider", consumer = "customer_consumer") 69 | // public RequestResponsePact pactForGetNonExistentAddressId(PactDslWithProvider builder) { 70 | // 71 | // return null; 72 | // } 73 | 74 | @Test 75 | @PactTestFor(pactMethod = "pactForGetExistingAddressId") 76 | public void testFor_GET_existingAddressId_shouldYieldExpectedAddressData(MockServer mockServer) { 77 | 78 | AddressServiceClient client = new AddressServiceClient(mockServer.getUrl()); 79 | 80 | Address address = client.getAddress(AddressId.EXISTING_ADDRESS_ID); 81 | 82 | Assertions.assertEquals(AddressId.EXISTING_ADDRESS_ID, address.getId()); 83 | } 84 | 85 | /** 86 | * TODO: uncomment the test method after completion of the pactForGetNonExistentAddressId() 87 | * method to add this interaction to the contract for the customer_consumer 88 | */ 89 | // @Test 90 | // @PactTestFor(pactMethod = "pactForGetNonExistentAddressId") 91 | // public void testFor_GET_nonExistentAddressId_shouldYieldHttp404(MockServer mockServer) { 92 | // 93 | // AddressServiceClient client = new AddressServiceClient(mockServer.getUrl()); 94 | // 95 | // Assertions.assertThrows(NotFoundException.class, () -> client.getAddress(AddressId.NON_EXISTING_ADDRESS_ID)); 96 | // } 97 | } 98 | --------------------------------------------------------------------------------