├── 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 |
--------------------------------------------------------------------------------