From 8a1307dc0543640448e04e102435f3d265b85165 Mon Sep 17 00:00:00 2001 From: Laurent Broudoux Date: Wed, 8 Jan 2025 16:40:58 +0100 Subject: [PATCH] Upgrade demo to microcks-testcontainers 0.3.0 features Signed-off-by: Laurent Broudoux --- pom.xml | 2 +- .../api/OrderControllerContractTests.java | 41 +++++++ .../order/client/PastryAPIClientTests.java | 12 ++ .../acme/order/service/OrderServiceTests.java | 23 +++- step-1-getting-started.md | 32 ++++-- step-4-write-rest-tests.md | 103 ++++++++++++++++++ step-5-write-async-tests.md | 45 +++++++- 7 files changed, 243 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index a73e7dd..e6a983c 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ io.github.microcks microcks-testcontainers - 0.2.10 + 0.3.0 test diff --git a/src/test/java/org/acme/order/api/OrderControllerContractTests.java b/src/test/java/org/acme/order/api/OrderControllerContractTests.java index 9abde98..cd19859 100644 --- a/src/test/java/org/acme/order/api/OrderControllerContractTests.java +++ b/src/test/java/org/acme/order/api/OrderControllerContractTests.java @@ -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; @@ -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 pairs = microcksEnsemble.getMicrocksContainer().getMessagesForTestCase(testResult, "POST /orders"); + for (RequestResponsePair pair : pairs) { + if ("201".equals(pair.getResponse().getStatus())) { + Map requestMap = mapper.readValue(pair.getRequest().getContent(), new TypeReference<>() {}); + Map responseMap = mapper.readValue(pair.getResponse().getContent(), new TypeReference<>() {}); + + List> requestPQ = (List>) requestMap.get("productQuantities"); + List> responsePQ = (List>) 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")); + } + } + } + } } diff --git a/src/test/java/org/acme/order/client/PastryAPIClientTests.java b/src/test/java/org/acme/order/client/PastryAPIClientTests.java index 289e5f0..42c082d 100644 --- a/src/test/java/org/acme/order/client/PastryAPIClientTests.java +++ b/src/test/java/org/acme/order/client/PastryAPIClientTests.java @@ -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 { @@ -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()); @@ -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"); } } diff --git a/src/test/java/org/acme/order/service/OrderServiceTests.java b/src/test/java/org/acme/order/service/OrderServiceTests.java index e5e6572..2abfb5f 100644 --- a/src/test/java/org/acme/order/service/OrderServiceTests.java +++ b/src/test/java/org/acme/order/service/OrderServiceTests.java @@ -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; @@ -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; @@ -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 events = microcksEnsemble.getMicrocksContainer() + .getEventMessagesForTestCase(testResult, "SUBSCRIBE orders-created"); + + assertEquals(1, events.size()); + + EventMessage message = events.get(0).getEventMessage(); + Map messageMap = new ObjectMapper().readValue(message.getContent(), new TypeReference<>() {}); + + // Properties from the event message should match the order. + assertEquals("Creation", messageMap.get("changeReason")); + Map orderMap = (Map) 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); } diff --git a/step-1-getting-started.md b/step-1-getting-started.md index 6b87e10..dfc6461 100644 --- a/step-1-getting-started.md +++ b/step-1-getting-started.md @@ -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 diff --git a/step-4-write-rest-tests.md b/step-4-write-rest-tests.md index b146535..4539bf3 100644 --- a/step-4-write-rest-tests.md +++ b/step-4-write-rest-tests.md @@ -109,6 +109,62 @@ sequenceDiagram PastryAPIClient-->>-PastryAPIClientTests: List ``` +### 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 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, @@ -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 pairs = microcksEnsemble.getMicrocksContainer().getMessagesForTestCase(testResult, "POST /orders"); + for (RequestResponsePair pair : pairs) { + if ("201".equals(pair.getResponse().getStatus())) { + Map requestMap = mapper.readValue(pair.getRequest().getContent(), new TypeReference<>() {}); + Map responseMap = mapper.readValue(pair.getResponse().getContent(), new TypeReference<>() {}); + + List> requestPQ = (List>) requestMap.get("productQuantities"); + List> responsePQ = (List>) 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) \ No newline at end of file diff --git a/step-5-write-async-tests.md b/step-5-write-async-tests.md index 810e516..cc7e08a 100644 --- a/step-5-write-async-tests.md +++ b/step-5-write-async-tests.md @@ -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 @@ -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 events = microcksEnsemble.getMicrocksContainer() + .getEventMessagesForTestCase(testResult, "SUBSCRIBE orders-created"); + + assertEquals(1, events.size()); + + EventMessage message = events.get(0).getEventMessage(); + Map messageMap = new ObjectMapper().readValue(message.getContent(), new TypeReference<>() {}); + + // Properties from the event message should match the order. + assertEquals("Creation", messageMap.get("changeReason")); + Map orderMap = (Map) 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: