-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Laurent Broudoux <[email protected]>
- Loading branch information
Showing
11 changed files
with
249 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Step 1: Getting Started | ||
|
||
Before getting started, let's make sure you have everything you need for running this demo. | ||
|
||
## Prerequisites | ||
|
||
### Install Go 1.23 or newer | ||
|
||
You'll need Go 1.23 or newer for this workshop. | ||
|
||
### Install Docker | ||
|
||
You need to have a [Docker](https://docs.docker.com/get-docker/) or [Podman](https://podman.io/) environment to use Testcontainers. | ||
|
||
```shell | ||
$ 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 | ||
OS/Arch: darwin/arm64 | ||
Context: desktop-linux | ||
|
||
Server: Docker Desktop 4.27.2 (137060) | ||
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 | ||
OS/Arch: linux/arm64 | ||
Experimental: false | ||
``` | ||
|
||
## Download the project | ||
|
||
Clone the [microcks-testcontainers-go-demo](https://github.com/microcks/microcks-testcontainers-go-demo) repository from GitHub to your computer: | ||
|
||
```shell | ||
git clone https://github.com/microcks/microcks-testcontainers-go-demo.git | ||
``` | ||
|
||
## Compile the project to download the dependencies | ||
|
||
With the Makefile: | ||
|
||
```shell | ||
make build | ||
``` | ||
|
||
### | ||
|
||
[Next](step-2-exploring-the-app.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# Step 2: Exploring the app | ||
|
||
This fictional application we're working on is a typical `Order Service` that can allow online, physical stores, or even | ||
partners to place orders for our fresh-backed pastries! For that, the `Order Service` is exposing a REST API to its consumers | ||
but also relies on an existing API we have [introduced in a previous post](https://medium.com/@lbroudoux/different-levels-of-api-contract-testing-with-microcks-ccc0847f8c97) 😉 | ||
|
||
![Order Service ecosystem](./assets/order-service-ecosystem.png) | ||
|
||
The `Order Service` application has been designed around 5 main components that are directly mapped on Spring Boot components and classes: | ||
* The [`OrderController`](internal/controller/order_controller.go) (in package `controller`) is responsible for exposing an `Order API` to the outer world. | ||
* The [`OrderService`](internal/service/order_service.go) is responsible for implementing the business logic around the creation of orders. | ||
* The [`PastryAPIClient`](internal/client/pastry_api.go) is responsible for calling the `Pastry API` in *Product Domain* and get details or list of pastries. | ||
* The [`OrderEventPublisher`](internal/service/order_event_publisher.go) is responsible for publishing a message on a `Kafka` topic when a new `Order` is created. | ||
* The [`OrderEventListener`](internal/service/order_event_listener.go) is responsible for consuming message on a `Kafka` topic when an `Order` has been reviewed. | ||
|
||
![Order Service architecture](./assets/order-service-architecture.png) | ||
|
||
Of course, this is a very naive vision of a real-life system as such an application would certainly pull out much more | ||
dependencies (like a `Payment Service`, a `Customer Service`, a `Shipping Service`, and much more) and offer more complex API. | ||
|
||
|
||
However, this situation is complex enough to highlight the two problems we're addressing: | ||
1) How to **efficiently set up a development environment** that depends on third-party API like the Pastry API? | ||
- You certainly want to avoid cloning this component repository and trying to figure out how to launch and configure it accordingly. | ||
- As a developer, developing your own mock of this service makes you also lose time and risk drifting from initial intent, | ||
2) How to **efficiently validate the conformance** of the `Order API` and `Order Events` against business expectations and API contracts? | ||
- Besides the core business logic, you might want to validate the network and protocol serialization layers as well as the respect of semantics. | ||
|
||
## Business logic | ||
|
||
This application must implement basic flows: | ||
* When creating a new [`Order`](internal/model/order.go), the service must check that the products are available before creating and persisting an order. Otherwise, order cannot be placed. | ||
* When the [`Order`](internal/model/order.go) is actually created, the service must also publish an [`OrderEvent`](internal/model/order.go) to a specific Kafka topic to propagate this information to other systems that will review the events, | ||
* When the [`OrderEvent`](internal/model/order.go) has been reviewed, a new message is published on another `Kafka` topic. The [`OrderEventListener`](internal/service/order_event_listener.go) must capture-it and update the corresponding [`Order`](internal/model/order.go) status using the service. | ||
|
||
## Flows specifications | ||
|
||
All the interactions are specified using API contracts: | ||
* The Order API is specified using the [`order-service-openapi.yaml`](testdata/order-service-openapi.yaml) OpenAPI specification, | ||
* The Pastry API is specified using the [`apipastries-openapi.yaml`](testdata/apipastries-openapi.yaml) OpenAPI specification, | ||
* The Order Events are specified using the [`order-events-asyncapi.yaml`](testdata/order-events-asyncapi.yaml) AsyncAPI specification. | ||
|
||
Those specifications will help us for two things: | ||
1) They will be used to provide simulations (or mocks) of third-parties systems - typically the Pastry API provider and the reviewer system that provides updates on `OrderEvents` | ||
2) They will be used to allow checking the conformance of the provided `Order API` and the published `Order Event` on order creation. | ||
|
||
### | ||
[Next](step-3-local-development-experience.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
# Step 3: Local development experience with Microcks | ||
|
||
Our application uses Kafka and external dependencies. | ||
|
||
Currently, if you run the application from your terminal, you will see the following error: | ||
|
||
```shell | ||
go run cmd/main.go | ||
|
||
Starting Microcks TestContainers Go Demo application... | ||
Connecting to Kafka server: localhost:9092 | ||
Connecting to Microcks Pastries: http://localhost:9090/rest/API+Pastries/0.0.1 | ||
%4|1732031480.769|CONFWARN|rdkafka#producer-2| [thrd:app]: Configuration property group.id is a consumer property and will be ignored by this producer instance | ||
%4|1732031480.769|CONFWARN|rdkafka#producer-2| [thrd:app]: Configuration property auto.offset.reset is a consumer property and will be ignored by this producer instance | ||
Microcks TestContainers Go Demo application is listening on localhost:9000 | ||
|
||
2024/11/19 16:51:20.772192 main.go:98: Starting application with Pastry API URL: http://localhost:9090/rest/API+Pastries/0.0.1, Kafka bootstrap: localhost:9092 | ||
2024/11/19 16:51:20.847780 order_event_listener.go:115: Error reading message: Subscribed topic not available: OrderEventsAPI-0.1.0-orders-reviewed: Broker: Unknown topic or partition | ||
``` | ||
|
||
To run the application locally, we need to have a Kafka broker up and running + the other dependencies corresponding to our Pastry API provider and reviewing system. | ||
|
||
Instead of installing these services on our local machine, or using Docker to run these services manually, | ||
we will use a utility tool with this simple command `microcks.sh`. Microcks docker-compose file (`microcks-docker-compose.yml`) | ||
has been configured to automatically import the `Order API` contract but also the `Pastry API` contracts. Both APIs are discovered on startup | ||
and Microcks UI should be available on `http://localhost:9090` in your browser: | ||
|
||
```shell | ||
$ ./microcks.sh | ||
==== OUTPUT ==== | ||
[+] Running 4/4 | ||
✔ Container microcks-testcontainers-node-nest-demo-microcks-1 Started 0.2s | ||
✔ Container microcks-kafka Started 0.2s | ||
✔ Container microcks-async-minion Started 0.4s | ||
✔ Container microcks-testcontainers-node-nest-demo-importer-1 Started 0.4s | ||
``` | ||
|
||
Because our `Order Service` application has been configured to talk to Microcks mocks (see the default settings in `src/pastry/pastry.module.ts`), | ||
you should be able to directly call the Order API and invoke the whole chain made of the 3 components: | ||
|
||
```sh | ||
$ curl -XPOST localhost:9000/api/orders -H 'Content-Type: application/json' \ | ||
-d '{"customerId": "lbroudoux", "productQuantities": [{"productName": "Millefeuille", "quantity": 1}], "totalPrice": 5.1}' -s | jq . | ||
==== OUTPUT ==== | ||
{ | ||
"customerId": "lbroudoux", | ||
"productQuantities": [ | ||
{ | ||
"productName": "Millefeuille", | ||
"quantity": 1 | ||
} | ||
], | ||
"totalPrice": 5.1, | ||
"id": "dded1111-8e99-4ba7-8755-e718972480e3", | ||
"status": "CREATED" | ||
} | ||
``` | ||
|
||
## Review application configuration under cmd/main.go | ||
|
||
In order to specify the dependant services we need, we use a `Config` structure that can be either populated from environment variables or defaults into `cmd/main.go`. | ||
|
||
The defaults bind our application to the services provided by Microcks or loaded via the `microcks-docker-compose.yml` file. | ||
|
||
```go | ||
const ( | ||
defaultPastryAPIURL = "http://localhost:9090/rest/API+Pastries/0.0.1" | ||
defaultKafkaBootstrap = "localhost:9092" | ||
shutdownTimeout = 15 * time.Second | ||
defaultOrdersTopic = "orders-created" | ||
defaultReviewedTopic = "OrderEventsAPI-0.1.0-orders-reviewed" | ||
) | ||
|
||
// loadConfig loads configuration from environment variables with defaults. | ||
func loadConfig() *Config { | ||
return &Config{ | ||
PastryAPIURL: getEnv("PASTRY_API_URL", defaultPastryAPIURL), | ||
KafkaBootstrap: getEnv("KAFKA_BOOTSTRAP_URL", defaultKafkaBootstrap), | ||
OrdersTopic: getEnv("ORDERS_TOPIC", defaultOrdersTopic), | ||
ReviewedTopic: getEnv("REVIEWED_TOPIC", defaultReviewedTopic), | ||
} | ||
} | ||
``` | ||
|
||
## Play with the API | ||
|
||
### Create an order | ||
|
||
```shell | ||
curl -XPOST localhost:9000/api/orders -H 'Content-type: application/json' \ | ||
-d '{"customerId": "lbroudoux", "productQuantities": [{"productName": "Millefeuille", "quantity": 1}], "totalPrice": 5.1}' -v | ||
``` | ||
|
||
You should get a response similar to the following: | ||
|
||
```shell | ||
< HTTP/1.1 201 | ||
< Content-Type: application/json | ||
< Transfer-Encoding: chunked | ||
< Date: Mon, 19 Nov 2024 17:15:42 GMT | ||
< | ||
* Connection #0 to host localhost left intact | ||
{"id":"2da3a517-9b3b-4788-81b5-b1a1aac71746","status":"CREATED","customerId":"lbroudoux","productQuantities":[{"productName":"Millefeuille","quantity":1}],"totalPrice":5.1}% | ||
``` | ||
|
||
Now test with something else, requesting for another Pastry: | ||
|
||
```shell | ||
curl -XPOST localhost:9000/api/orders -H 'Content-type: application/json' \ | ||
-d '{"customerId": "lbroudoux", "productQuantities": [{"productName": "Eclair Chocolat", "quantity": 1}], "totalPrice": 4.1}' -v | ||
``` | ||
|
||
This time you get another "exception" response: | ||
|
||
```shell | ||
< HTTP/1.1 422 | ||
< Content-Type: application/json | ||
< Transfer-Encoding: chunked | ||
< Date: Mon, 19 Nov 2024 17:19:08 GMT | ||
< | ||
* Connection #0 to host localhost left intact | ||
{"productName":"Eclair Chocolat","details":"Pastry Eclair Chocolat is not available"}% | ||
``` | ||
|
||
and this is because Microcks has created different simulations for the Pastry API 3rd party API based on API artifacts we loaded. | ||
Check the `testdata/apipastries-openapi.yaml` and `testdata/apipastries-postman-collection.json` files to get details. | ||
|
||
### | ||
[Next](step-4-write-rest-tests.md) |