Skip to content

Commit

Permalink
Upgrade demo to microcks-testcontainers 0.3.0 features
Browse files Browse the repository at this point in the history
Signed-off-by: Laurent Broudoux <[email protected]>
  • Loading branch information
lbroudoux committed Jan 8, 2025
1 parent ca62a3e commit 8a1307d
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 15 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
<dependency>
<groupId>io.github.microcks</groupId>
<artifactId>microcks-testcontainers</artifactId>
<version>0.2.10</version>
<version>0.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
41 changes: 41 additions & 0 deletions src/test/java/org/acme/order/api/OrderControllerContractTests.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package org.acme.order.api;

import io.github.microcks.testcontainers.model.RequestResponsePair;
import io.github.microcks.testcontainers.model.TestRequest;
import io.github.microcks.testcontainers.model.TestResult;
import io.github.microcks.testcontainers.model.TestRunnerType;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.acme.order.BaseIntegrationTest;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand All @@ -32,4 +37,40 @@ void testOpenAPIContract() throws Exception {
assertTrue(testResult.isSuccess());
assertEquals(1, testResult.getTestCaseResults().size());
}

@Test
void testOpenAPIContractAndBusinessConformance() throws Exception {
// Ask for an Open API conformance to be launched.
TestRequest testRequest = new TestRequest.Builder()
.serviceId("Order Service API:0.1.0")
.runnerType(TestRunnerType.OPEN_API_SCHEMA.name())
.testEndpoint("http://host.testcontainers.internal:" + port + "/api")
.build();

TestResult testResult = microcksEnsemble.getMicrocksContainer().testEndpoint(testRequest);

// You may inspect complete response object with following:
ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(testResult));

assertTrue(testResult.isSuccess());
assertEquals(1, testResult.getTestCaseResults().size());

// You may also check business conformance.
List<RequestResponsePair> pairs = microcksEnsemble.getMicrocksContainer().getMessagesForTestCase(testResult, "POST /orders");
for (RequestResponsePair pair : pairs) {
if ("201".equals(pair.getResponse().getStatus())) {
Map<String, Object> requestMap = mapper.readValue(pair.getRequest().getContent(), new TypeReference<>() {});
Map<String, Object> responseMap = mapper.readValue(pair.getResponse().getContent(), new TypeReference<>() {});

List<Map<String, Object>> requestPQ = (List<Map<String, Object>>) requestMap.get("productQuantities");
List<Map<String, Object>> responsePQ = (List<Map<String, Object>>) responseMap.get("productQuantities");

assertEquals(requestPQ.size(), responsePQ.size());
for (int i = 0; i < requestPQ.size(); i++) {
assertEquals(requestPQ.get(i).get("productName"), responsePQ.get(i).get("productName"));
}
}
}
}
}
12 changes: 12 additions & 0 deletions src/test/java/org/acme/order/client/PastryAPIClientTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class PastryAPIClientTests extends BaseIntegrationTest {

Expand All @@ -25,10 +26,17 @@ void testGetPastries() {

pastries = client.listPastries("L");
assertEquals(2, pastries.size());

// Check that the mock API has really been invoked.
boolean mockInvoked = microcksEnsemble.getMicrocksContainer().verify("API Pastries", "0.0.1");
assertTrue(mockInvoked, "Mock API not invoked");
}

@Test
void testGetPastry() {
// Get the number of invocations before our test.
long beforeMockInvocations = microcksEnsemble.getMicrocksContainer().getServiceInvocationsCount("API Pastries", "0.0.1");

// Test our API client and check that arguments and responses are correctly serialized.
Pastry pastry = client.getPastry("Millefeuille");
assertEquals("Millefeuille", pastry.name());
Expand All @@ -41,5 +49,9 @@ void testGetPastry() {
pastry = client.getPastry("Eclair Chocolat");
assertEquals("Eclair Chocolat", pastry.name());
assertEquals("unknown", pastry.status());

// Check our mock API has been invoked the correct number of times.
long afterMockInvocations = microcksEnsemble.getMicrocksContainer().getServiceInvocationsCount("API Pastries", "0.0.1");
assertEquals(3, afterMockInvocations - beforeMockInvocations, "Mock API not invoked the correct number of times");
}
}
23 changes: 22 additions & 1 deletion src/test/java/org/acme/order/service/OrderServiceTests.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package org.acme.order.service;

import io.github.microcks.testcontainers.model.EventMessage;
import io.github.microcks.testcontainers.model.TestRequest;
import io.github.microcks.testcontainers.model.TestResult;
import io.github.microcks.testcontainers.model.TestRunnerType;
import io.github.microcks.testcontainers.model.UnidirectionalEvent;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.acme.order.BaseIntegrationTest;
import org.acme.order.service.model.Order;
import org.acme.order.service.model.OrderInfo;
Expand All @@ -17,6 +21,7 @@

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -69,7 +74,23 @@ void testEventIsPublishedWhenOrderIsCreated() {
assertFalse(testResult.getTestCaseResults().isEmpty());
assertEquals(1, testResult.getTestCaseResults().get(0).getTestStepResults().size());

System.err.println(microcksEnsemble.getAsyncMinionContainer().getLogs());
//System.err.println(microcksEnsemble.getAsyncMinionContainer().getLogs());

// Check the content of the emitted event, read from Kafka topic.
List<UnidirectionalEvent> events = microcksEnsemble.getMicrocksContainer()
.getEventMessagesForTestCase(testResult, "SUBSCRIBE orders-created");

assertEquals(1, events.size());

EventMessage message = events.get(0).getEventMessage();
Map<String, Object> messageMap = new ObjectMapper().readValue(message.getContent(), new TypeReference<>() {});

// Properties from the event message should match the order.
assertEquals("Creation", messageMap.get("changeReason"));
Map<String, Object> orderMap = (Map<String, Object>) messageMap.get("order");
assertEquals("123-456-789", orderMap.get("customerId"));
assertEquals(8.4, orderMap.get("totalPrice"));
assertEquals(2, ((List<?>) orderMap.get("productQuantities")).size());
} catch (Exception e) {
fail("No exception should be thrown when testing Kafka publication", e);
}
Expand Down
32 changes: 20 additions & 12 deletions step-1-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,32 @@ You need to have a [Docker](https://docs.docker.com/get-docker/) or [Podman](htt
$ docker version

Client:
Cloud integration: v1.0.35+desktop.10
Version: 25.0.3
API version: 1.44
Go version: go1.21.6
Git commit: 4debf41
Built: Tue Feb 6 21:13:26 2024
Version: 27.3.1
API version: 1.47
Go version: go1.22.7
Git commit: ce12230
Built: Fri Sep 20 11:38:18 2024
OS/Arch: darwin/arm64
Context: desktop-linux

Server: Docker Desktop 4.27.2 (137060)
Server: Docker Desktop 4.36.0 (175267)
Engine:
Version: 25.0.3
API version: 1.44 (minimum version 1.24)
Go version: go1.21.6
Git commit: f417435e5f6216828dec57958c490c4f8bae4f98
Built: Wed Feb 7 00:39:16 2024
Version: 27.3.1
API version: 1.47 (minimum version 1.24)
Go version: go1.22.7
Git commit: 41ca978
Built: Fri Sep 20 11:41:19 2024
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.7.21
GitCommit: 472731909fa34bd7bc9c087e4c27943f9835f111
runc:
Version: 1.1.13
GitCommit: v1.1.13-0-g58aa920
docker-init:
Version: 0.19.0
GitCommit: de40ad0
```

## Download the project
Expand Down
103 changes: 103 additions & 0 deletions step-4-write-rest-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,62 @@ sequenceDiagram
PastryAPIClient-->>-PastryAPIClientTests: List<Pastry>
```

### Bonus step - Check the mock endpoints are actually used

While the above test is a good start, it doesn't actually check that the mock endpoints are being used. In a more complex application, it's possible
that the client is not correctly configured or use some cache or other mechanism that would bypass the mock endpoints. In order to check that you
can actually use the `verify()` method available on the Microcks container:

```java
@Test
public void testGetPastries() {
// Test our API client and check that arguments and responses are correctly serialized.
List<Pastry> pastries = client.listPastries("S");
assertEquals(1, pastries.size());

pastries = client.listPastries("M");
assertEquals(2, pastries.size());

pastries = client.listPastries("L");
assertEquals(2, pastries.size());

// Check that the mock API has really been invoked.
boolean mockInvoked = microcksEnsemble.getMicrocksContainer().verify("API Pastries", "0.0.1");
assertTrue(mockInvoked, "Mock API not invoked");
}
```

`verify()` takes the target API name and version as arguments and returns a boolean indicating if the mock has been invoked. This is a good way to
ensure that the mock endpoints are actually being used in your test.

If you need finer-grained control, you can also check the number of invocations with `getServiceInvocationsCount()`. This way you can check that
the mock has been invoked the correct number of times:

```java
@Test
void testGetPastry() {
// Get the number of invocations before our test.
long beforeMockInvocations = microcksEnsemble.getMicrocksContainer().getServiceInvocationsCount("API Pastries", "0.0.1");

// Test our API client and check that arguments and responses are correctly serialized.
Pastry pastry = client.getPastry("Millefeuille");
assertEquals("Millefeuille", pastry.name());
assertEquals("available", pastry.status());

pastry = client.getPastry("Eclair Cafe");
assertEquals("Eclair Cafe", pastry.name());
assertEquals("available", pastry.status());

pastry = client.getPastry("Eclair Chocolat");
assertEquals("Eclair Chocolat", pastry.name());
assertEquals("unknown", pastry.status());

// Check our mock API has been invoked the correct number of times.
long afterMockInvocations = microcksEnsemble.getMicrocksContainer().getServiceInvocationsCount("API Pastries", "0.0.1");
assertEquals(3, afterMockInvocations - beforeMockInvocations, "Mock API not invoked the correct number of times");
}
```

## Second Test - Verify the technical conformance of Order Service API

The 2nd thing we want to validate is the conformance of the `Order API` we'll expose to consumers. In this section and the next one,
Expand Down Expand Up @@ -245,6 +301,53 @@ reuses Postman collection constraints.
You're now sure that beyond the technical conformance, the `Order Service` also behaves as expected regarding business
constraints.

### Bonus step - Verify the business conformance of Order Service API in pure Java

Even if the Postman Collection runner is a great way to validate business conformance, you may want to do it in pure Java.
This is possible by retrieving the messages exchanged during the test and checking their content. Let's review the `testOpenAPIContractAndBusinessConformance()`
test in class `OrderControllerContractTests` under `src/test/java/org/acme/order/api`:

```java
@Test
void testOpenAPIContractAndBusinessConformance() throws Exception {
// Ask for an Open API conformance to be launched.
TestRequest testRequest = new TestRequest.Builder()
.serviceId("Order Service API:0.1.0")
.runnerType(TestRunnerType.OPEN_API_SCHEMA.name())
.testEndpoint("http://host.testcontainers.internal:" + port + "/api")
.build();

TestResult testResult = microcksEnsemble.getMicrocksContainer().testEndpoint(testRequest);

assertTrue(testResult.isSuccess());
assertEquals(1, testResult.getTestCaseResults().size());

// You may also check business conformance.
List<RequestResponsePair> pairs = microcksEnsemble.getMicrocksContainer().getMessagesForTestCase(testResult, "POST /orders");
for (RequestResponsePair pair : pairs) {
if ("201".equals(pair.getResponse().getStatus())) {
Map<String, Object> requestMap = mapper.readValue(pair.getRequest().getContent(), new TypeReference<>() {});
Map<String, Object> responseMap = mapper.readValue(pair.getResponse().getContent(), new TypeReference<>() {});

List<Map<String, Object>> requestPQ = (List<Map<String, Object>>) requestMap.get("productQuantities");
List<Map<String, Object>> responsePQ = (List<Map<String, Object>>) responseMap.get("productQuantities");

assertEquals(requestPQ.size(), responsePQ.size());
for (int i = 0; i < requestPQ.size(); i++) {
assertEquals(requestPQ.get(i).get("productName"), responsePQ.get(i).get("productName"));
}
}
}
}
```

This test is a bit more complex than the previous ones. It first asks for an OpenAPI conformance test to be launched and then retrieves the messages
to check business conformance, following the same logic that was implemented into the Postman Collection snippet.

It uses the `getMessagesForTestCase()` method to retrieve the messages exchanged during the test and then checks the content. While this is done
in pure Java here, you may use the tool or library of your choice like [JSONAssert](https://github.com/skyscreamer/JSONassert),
[Cucumber](https://cucumber.io/docs/installation/java/) or others


###
[Next](step-5-write-async-tests.md)
45 changes: 44 additions & 1 deletion step-5-write-async-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Things are a bit more complex here, but we'll walk through step-by-step:

The sequence diagram below details the test sequence. You'll see 2 parallel blocks being executed:
* One that corresponds to Microcks test - where it connects and listen for Kafka messages,
* One that corresponds to the `OrderService` invokation that is expected to trigger a message on Kafka.
* One that corresponds to the `OrderService` invocation that is expected to trigger a message on Kafka.

```mermaid
sequenceDiagram
Expand All @@ -113,6 +113,49 @@ Because the test is a success, it means that Microcks has received an `OrderEven
conformance with the AsyncAPI contract or this event-driven architecture. So you're sure that all your Spring Boot configuration, Kafka JSON serializer
configuration and network communication are actually correct!

### Bonus step - Verify the event content

So you're now sure that an event has been sent to Kafka and that it's valid regarding the AsyncAPI contract. But what about the content
of this event? If you want to go further and check the content of the event, you can do it by asking Microcks the events read during the
test execution and actually check their content. This can be done adding a few lines of code:

```java
@Test
void testEventIsPublishedWhenOrderIsCreated() {
// [...] Unchanged comparing previous step.

try {
// [...] Unchanged comparing previous step.

// Get the Microcks test result.
TestResult testResult = testRequestFuture.get();

// [...] Unchanged comparing previous step.

// Check the content of the emitted event, read from Kafka topic.
List<UnidirectionalEvent> events = microcksEnsemble.getMicrocksContainer()
.getEventMessagesForTestCase(testResult, "SUBSCRIBE orders-created");

assertEquals(1, events.size());

EventMessage message = events.get(0).getEventMessage();
Map<String, Object> messageMap = new ObjectMapper().readValue(message.getContent(), new TypeReference<>() {});

// Properties from the event message should match the order.
assertEquals("Creation", messageMap.get("changeReason"));
Map<String, Object> orderMap = (Map<String, Object>) messageMap.get("order");
assertEquals("123-456-789", orderMap.get("customerId"));
assertEquals(8.4, orderMap.get("totalPrice"));
assertEquals(2, ((List<?>) orderMap.get("productQuantities")).size());
} catch (Exception e) {
fail("No exception should be thrown when testing Kafka publication", e);
}
}
```

Here, we're using the `getEventMessagesForTestCase()` method on the Microcks container to retrieve the messages read during the test execution.
Using the wrapped `EventMessage` class, we can then check the content of the message and assert that it matches the order we've created.

## Second Test - Verify our OrderEventListener is processing events

In this section, we'll focus on testing the `Event Consumer` + `Order Service` components of our application:
Expand Down

0 comments on commit 8a1307d

Please sign in to comment.