Skip to content

Commit 3420ff5

Browse files
committed
feat: Upgrading to microcks-testcontainers 0.3.0 with new features
Signed-off-by: Laurent Broudoux <[email protected]>
1 parent 93f0ac6 commit 3420ff5

9 files changed

+276
-31
lines changed

package-lock.json

+14-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
"rxjs": "^7.8.1"
3232
},
3333
"devDependencies": {
34-
"@microcks/microcks-testcontainers": "0.2.6",
34+
"@microcks/microcks-testcontainers": "0.3.0",
3535
"@nestjs/cli": "^10.4.2",
3636
"@nestjs/schematics": "^10.2.3",
3737
"@nestjs/testing": "^10.4.2",
38-
"@testcontainers/kafka": "^10.7.2",
38+
"@testcontainers/kafka": "^10.10.4",
3939
"@types/express": "^4.17.17",
4040
"@types/jest": "^29.5.13",
4141
"@types/node": "^20.14.5",

src/pastry/pastry.service.spec.ts

+11
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,16 @@ describe('PastryService', () => {
5050
pastry = await service.getPastry('Eclair Chocolat');
5151
expect(pastry.name).toBe("Eclair Chocolat");
5252
expect(pastry.status).toBe("unknown");
53+
54+
// Check that the mock API has really been invoked.
55+
let mockInvoked: boolean = await container.verify("API Pastries", "0.0.1");
56+
expect(mockInvoked).toBe(true);
5357
});
5458

5559
it('should retrieve pastries by size', async () => {
60+
// Get the number of invocations before our test.
61+
let beforeMockInvocations: number = await container.getServiceInvocationsCount("API Pastries", "0.0.1");
62+
5663
let pastries: Pastry[] = await service.getPastries('S');
5764
expect(pastries.length).toBe(1);
5865

@@ -61,5 +68,9 @@ describe('PastryService', () => {
6168

6269
pastries = await service.getPastries('L');
6370
expect(pastries.length).toBe(2);
71+
72+
// Check our mock API has been invoked the correct number of times.
73+
let afterMockInvocations: number = await container.getServiceInvocationsCount("API Pastries", "0.0.1");
74+
expect(afterMockInvocations - beforeMockInvocations).toBe(3);
6475
});
6576
});

step-1-getting-started.md

+20-12
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,32 @@ You need to have a [Docker](https://docs.docker.com/get-docker/) or [Podman](htt
1717
$ docker version
1818

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

29-
Server: Docker Desktop 4.27.2 (137060)
28+
Server: Docker Desktop 4.36.0 (175267)
3029
Engine:
31-
Version: 25.0.3
32-
API version: 1.44 (minimum version 1.24)
33-
Go version: go1.21.6
34-
Git commit: f417435e5f6216828dec57958c490c4f8bae4f98
35-
Built: Wed Feb 7 00:39:16 2024
30+
Version: 27.3.1
31+
API version: 1.47 (minimum version 1.24)
32+
Go version: go1.22.7
33+
Git commit: 41ca978
34+
Built: Fri Sep 20 11:41:19 2024
3635
OS/Arch: linux/arm64
3736
Experimental: false
37+
containerd:
38+
Version: 1.7.21
39+
GitCommit: 472731909fa34bd7bc9c087e4c27943f9835f111
40+
runc:
41+
Version: 1.1.13
42+
GitCommit: v1.1.13-0-g58aa920
43+
docker-init:
44+
Version: 0.19.0
45+
GitCommit: de40ad0
3846
```
3947

4048
## Download the project

step-2-exploring-the-app.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ but also relies on an existing API we have [introduced in a previous post](https
66

77
![Order Service ecosystem](./assets/order-service-ecosystem.png)
88

9-
The `Order Service` application has been designed around 5 main components that are directly mapped on Spring Boot components and classes:
9+
The `Order Service` application has been designed around 5 main components that are directly mapped on NestJS components and classes:
1010
* The [`OrderController`](src/order/order.controller.ts) is responsible for exposing an `Order API` to the outer world.
1111
* The [`OrderService`](src/order/order.service.ts) is responsible for implementing the business logic around the creation of orders.
1212
* The [`PastryAPIClient`](src/pastry/pastry.service.ts) is responsible for calling the `Pastry API` in *Product Domain* and get details or list of pastries.

step-4-write-rest-tests.md

+107-1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,61 @@ sequenceDiagram
9595
PastryAPIClient-->>-PastryAPIClientTests: List<Pastry>
9696
```
9797

98+
### 🎁 Bonus step - Check the mock endpoints are actually used
99+
100+
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
101+
possible that the client is not correctly configured or use some cache or other mechanism that would bypass the mock endpoints.
102+
In order to check that you can actually use the `verify()` method available on the Microcks container:
103+
104+
```ts
105+
it('should retrieve pastry by name', async () => {
106+
let pastry: Pastry = await service.getPastry('Millefeuille');
107+
expect(pastry.name).toBe("Millefeuille");
108+
expect(pastry.status).toBe("available");
109+
110+
pastry = await service.getPastry('Eclair Cafe');
111+
expect(pastry.name).toBe("Eclair Cafe");
112+
expect(pastry.status).toBe("available");
113+
114+
pastry = await service.getPastry('Eclair Chocolat');
115+
expect(pastry.name).toBe("Eclair Chocolat");
116+
expect(pastry.status).toBe("unknown");
117+
118+
// Check that the mock API has really been invoked.
119+
let mockInvoked: boolean = await container.verify("API Pastries", "0.0.1");
120+
expect(mockInvoked).toBe(true);
121+
});
122+
```
123+
124+
`verify()` takes the target API name and version as arguments and returns a boolean indicating if the mock has been invoked.
125+
This is a good way to ensure that the mock endpoints are actually being used in your test.
126+
127+
If you need finer-grained control, you can also check the number of invocations with `getServiceInvocationsCount()`. This way you can check
128+
that the mock has been invoked the correct number of times:
129+
130+
```ts
131+
it('should retrieve pastries by size', async () => {
132+
// Get the number of invocations before our test.
133+
let beforeMockInvocations: number = await container.getServiceInvocationsCount("API Pastries", "0.0.1");
134+
135+
let pastries: Pastry[] = await service.getPastries('S');
136+
expect(pastries.length).toBe(1);
137+
138+
pastries = await service.getPastries('M');
139+
expect(pastries.length).toBe(2);
140+
141+
pastries = await service.getPastries('L');
142+
expect(pastries.length).toBe(2);
143+
144+
// Check our mock API has been invoked the correct number of times.
145+
let afterMockInvocations: number = await container.getServiceInvocationsCount("API Pastries", "0.0.1");
146+
expect(afterMockInvocations - beforeMockInvocations).toBe(3);
147+
});
148+
```
149+
150+
This is a super powerful way to ensure that your application logic (caching, no caching, etc.) is correctly implemented and
151+
use the mock endpoints when required 🎉
152+
98153
## Second Test - Verify the technical conformance of Order Service API
99154

100155
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,
@@ -133,7 +188,7 @@ test we want to run:
133188
* We ask for testing our endpoint against the service interface of `Order Service API` in version `0.1.0`.
134189
These are the identifiers found in the `order-service-openapi.yml` file.
135190
* We ask Microcks to validate the `OpenAPI Schema` conformance by specifying a `runnerType`.
136-
* We ask Microcks to validate the localhost endpoint on the dynamic port provided by the Spring Test (we use the `host.testcontainers.internal` alias for that).
191+
* We ask Microcks to validate the localhost endpoint on the dynamic port provided by the `beforeAll()` function (we use the `host.testcontainers.internal` alias for that).
137192

138193
Finally, we're retrieving a `TestResult` from Microcks containers, and we can assert stuffs on this result, checking it's a success.
139194

@@ -215,5 +270,56 @@ reuses Postman collection constraints.
215270
You're now sure that beyond the technical conformance, the `Order Service` also behaves as expected regarding business
216271
constraints.
217272

273+
### 🎁 Bonus step - Verify the business conformance of Order Service API in pure JS/TS
274+
275+
Even if the Postman Collection runner is a great way to validate business conformance, you may want to do it in pure JavaScript.
276+
This is possible by retrieving the messages exchanged during the test and checking their content. Let's review the
277+
`should conform to OpenAPI spec and Business rules` test under `test/orders.api.e2e-spec.ts`:
278+
279+
```ts
280+
it('should conform to OpenAPI spec and Business rules', async () => {
281+
var testRequest: TestRequest = {
282+
serviceId: "Order Service API:0.1.0",
283+
runnerType: TestRunnerType.OPEN_API_SCHEMA,
284+
testEndpoint: "http://host.testcontainers.internal:" + appPort,
285+
timeout: 3000
286+
};
287+
288+
var testResult = await container.testEndpoint(testRequest);
289+
290+
console.log(JSON.stringify(testResult, null, 2));
291+
292+
expect(testResult.success).toBe(true);
293+
expect(testResult.testCaseResults.length).toBe(1);
294+
expect(testResult.testCaseResults[0].testStepResults.length).toBe(2);
295+
296+
// You may also check business conformance.
297+
let pairs: RequestResponsePair[] = await container.getMessagesForTestCase(testResult, "POST /orders");
298+
299+
for (let i=0; i<pairs.length; i++) {
300+
const pair = pairs[i];
301+
if (pair.response.status === "201") {
302+
const requestPQS = JSON.parse(pair.request.content).productQuantities;
303+
const responsePQS = JSON.parse(pair.response.content).productQuantities;
304+
305+
expect(responsePQS).toBeDefined();
306+
expect(responsePQS.length).toBe(requestPQS.length);
307+
for (let j=0; j<requestPQS.length; j++) {
308+
const requestPQ = requestPQS[j];
309+
const responsePQ = responsePQS[j];
310+
expect(responsePQ.productName).toBe(requestPQ.productName);
311+
}
312+
}
313+
}
314+
});
315+
```
316+
317+
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
318+
the messages to check business conformance, following the same logic that was implemented into the Postman Collection snippet.
319+
320+
It uses the `getMessagesForTestCase()` method to retrieve the messages exchanged during the test and then checks the content. While this
321+
is done in pure JavaScript here, you may use the tool or library of your choice like [Cucumber](https://cucumber.io/docs/installation/javascript/)
322+
or others.
323+
218324
###
219325
[Next](step-5-write-async-tests.md)

step-5-write-async-tests.md

+69-1
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,43 @@ sequenceDiagram
151151
```
152152

153153
Because the test is a success, it means that Microcks has received an `OrderEvent` on the specified topic and has validated the message
154-
conformance with the AsyncAPI contract or this event-driven architecture. So you're sure that all your Spring Boot configuration, Kafka JSON serializer
154+
conformance with the AsyncAPI contract or this event-driven architecture. So you're sure that all your NestJS configuration, Kafka JSON serializer
155155
configuration and network communication are actually correct!
156156

157+
### 🎁 Bonus step - Verify the event content
158+
159+
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
160+
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
161+
test execution and actually check their content. This can be done adding a few lines of code:
162+
163+
```ts
164+
it ('should publish an Event when Order is created', async () => {
165+
// [...] Unchanged comparing previous step.
166+
167+
// Get the Microcks test result.
168+
var testResult = await testResultPromise;
169+
170+
// [...] Unchanged comparing previous step.
171+
172+
// Check the content of the emitted event, read from Kafka topic.
173+
let events: UnidirectionalEvent[] = await ensemble.getMicrocksContainer().getEventMessagesForTestCase(testResult, "SUBSCRIBE orders-created");
174+
175+
expect(events.length).toBe(1);
176+
177+
let message: EventMessage = events[0].eventMessage;
178+
let orderEvent = JSON.parse(message.content);
179+
180+
expect(orderEvent.changeReason).toBe('Creation');
181+
let order = orderEvent.order;
182+
expect(order.customerId).toBe("123-456-789");
183+
expect(order.totalPrice).toBe(8.4);
184+
expect(order.productQuantities.length).toBe(2);
185+
});
186+
```
187+
188+
Here, we're using the `getEventMessagesForTestCase()` method on the Microcks container to retrieve the messages read during the test execution.
189+
Using the wrapped `EventMessage` class, we can then check the content of the message and assert that it matches the order we've created.
190+
157191
## Second Test - Verify our OrderEventListener is processing events
158192

159193
In this section, we'll focus on testing the `Event Consumer` + `Order Service` components of our application:
@@ -164,7 +198,41 @@ The final thing we want to test here is that our `OrderEventListener` component
164198
for consuming messages, for de-serializing them into correct Java objects and for triggering the processing on the `OrderService`.
165199
That's a lot to do and can be quite complex! But things remain very simple with Microcks 😉
166200

201+
Let's review the test spec `order-event.listener.e2e-spec.ts` under `test`.
167202

203+
```ts
204+
it ('should consume an Event and process Service', async () => {
205+
let retry = 0;
206+
207+
while (retry < 10) {
208+
try {
209+
let order = orderService.getOrder('123-456-789');
210+
if (orderIsValid(order)) {
211+
break;
212+
}
213+
} catch (e) {
214+
if (e instanceof OrderNotFoundException) {
215+
// Continue until we get the end of the poll loop.
216+
} else {
217+
// Exit here.
218+
throw e;
219+
}
220+
}
221+
await delay(500);
222+
retry++
223+
}
224+
});
225+
226+
function orderIsValid(order: Order) : boolean {
227+
if (order) {
228+
expect(order.customerId).toBe('lbroudoux');
229+
expect(order.status).toBe(OrderStatus.VALIDATED);
230+
expect(order.productQuantities.length).toBe(2);
231+
return true;
232+
}
233+
return false;
234+
}
235+
```
168236

169237
To fully understand this test, remember that as soon as you're launching the test, we start Kafka and Microcks containers and that Microcks
170238
is immediately starting publishing mock messages on this broker. So this test actually starts with a waiting loop, just checking that the

0 commit comments

Comments
 (0)