This workshop should take about 2 hours, depending on how deep you want to go into each topic.
This workshop is setup with a number of steps that can be run through. Each step is in a branch, so to run through a
step of the workshop just check out the branch for that step (i.e. git checkout step1
).
- JDK 17+
- Docker for step 11
- step 1: Simple Consumer calling Provider
- step 2: Client Tested but integration fails
- step 3: Pact to the rescue
- step 4: Verify the provider
- step 5: Back to the client we go
- step 6: Consumer updates contract for missing products
- step 7: Adding the missing states
- step 8: Authorization
- step 9: Implement authorisation on the provider
- step 10: Request Filters on the Provider
- step 11: Using a Pact Broker
NOTE: Each step is tied to, and must be run within, a git branch, allowing you to progress through each stage incrementally. For example, to move to step 2 run the following: git checkout step2
There are two components in scope for our workshop.
- Product Catalog application (Consumer). It provides a console interface to query the Product service for product information.
- Product Service (Provider). Provides useful things about products, such as listing all products and getting the details of an individual product.
We need to first create an HTTP client to make the calls to our provider service:
The Consumer has implemented the product service client which has the following:
GET /products
- Retrieve all productsGET /products/{id}
- Retrieve a single product by ID
The diagram below highlights the interaction for retrieving a product with ID 10:
You can see the client interface we created in consumer/src/main/au/com/dius/pactworkshop/consumer/ProductService.java
:
@Service
public class ProductService {
private final RestTemplate restTemplate;
@Autowired
public ProductService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public List<Product> getAllProducts() {
return restTemplate.exchange("/products",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Product>>(){}).getBody();
}
public Product getProduct(String id) {
return restTemplate.getForEntity("/products/{id}", Product.class, id).getBody();
}
}
We can run the client with ./gradlew consumer:bootRun
- it should fail with the error below, because the Provider is not running.
Caused by: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8085/products": Connection refused: connect; nested exception is java.net.ConnectException: Connection refused: connect
Move on to step 2
Now let's create a basic test for our API client. We're going to check 2 things:
- That our client code hits the expected endpoint
- That the response is marshalled into an object that is usable, with the correct ID
You can see the client interface test we created in consumer/src/test/java/au/com/dius/pactworkshop/consumer/ProductServiceTest.java
:
class ProductServiceTest {
private WireMockServer wireMockServer;
private ProductService productService;
@BeforeEach
void setUp() {
wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
RestTemplate restTemplate = new RestTemplateBuilder()
.rootUri(wireMockServer.baseUrl())
.build();
productService = new ProductService(restTemplate);
}
@AfterEach
void tearDown() {
wireMockServer.stop();
}
@Test
void getAllProducts() {
wireMockServer.stubFor(get(urlPathEqualTo("/products"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("[" +
"{\"id\":\"9\",\"type\":\"CREDIT_CARD\",\"name\":\"GEM Visa\",\"version\":\"v2\"},"+
"{\"id\":\"10\",\"type\":\"CREDIT_CARD\",\"name\":\"28 Degrees\",\"version\":\"v1\"}"+
"]")));
List<Product> expected = Arrays.asList(new Product("9", "CREDIT_CARD", "GEM Visa", "v2"),
new Product("10", "CREDIT_CARD", "28 Degrees", "v1"));
List<Product> products = productService.getAllProducts();
assertEquals(expected, products);
}
@Test
void getProductById() {
wireMockServer.stubFor(get(urlPathEqualTo("/products/50"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":\"50\",\"type\":\"CREDIT_CARD\",\"name\":\"28 Degrees\",\"version\":\"v1\"}")));
Product expected = new Product("50", "CREDIT_CARD", "28 Degrees", "v1");
Product product = productService.getProduct("50");
assertEquals(expected, product);
}
}
Let's run this test and see it all pass:
> ./gradlew consumer:test
BUILD SUCCESSFUL in 2s
Meanwhile, our provider team has started building out their API in parallel. Let's run our website against our provider (you'll need two terminals to do this):
# Terminal 1
❯ ./gradlew provider:bootRun
...
...
Tomcat started on port(s): 8085 (http) with context path ''
Started ProviderApplication in 1.67 seconds (JVM running for 2.039)
# Terminal 2
> ./gradlew consumer:bootRun --console plain
...
...
Started ConsumerApplication in 1.106 seconds (JVM running for 1.62)
Products
--------
1) Gem Visa
2) MyFlexiPay
3) 28 Degrees
Select item to view details:
You should now see 3 different products. Choosing an index number should display detailed product information.
Let's see what happens!
Doh! We are getting 404 every time we try to view detailed product information. On closer inspection, the provider only knows about /product/{id}
and /products
.
We need to have a conversation about what the endpoint should be, but first...
Move on to step 3