Skip to content

Commit

Permalink
chore: Initializing documentation
Browse files Browse the repository at this point in the history
Signed-off-by: Laurent Broudoux <[email protected]>
  • Loading branch information
lbroudoux committed Nov 19, 2024
1 parent ed27ca2 commit 3dabb49
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 56 deletions.
71 changes: 15 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,68 +1,27 @@
# microcks-testcontainers-go-demo
# Microcks Testcontainers Go Demo

Go demonstration app on how to use Microcks Testcontainers in your dev/test workflow
![Microcks Testcontainers Go demo](./assets/microcks-testcontainers-go-demo.png)

This application is a demonstration on how to integrate Microcks via Testcontainers within your development inner-loop.

## Basic commands
You will work with a Go application and explore how to:
* Use Microcks for **provisioning third-party API mocks**,
* Use Microcks for **simulating external Kafka events publishers**,
* Write tests using Microcks **contract-testing** features for both **REST/OpenAPI based APIs and Events/AsyncAPI** based messaging

Start launching Microcks locally:
## Table of contents

```sh
$ ./microcks.sh
==== OUTPUT ====
[...]
```

In another terminal, run the application:
* [Step 1: Getting Started](step-1-getting-started.md)
* [Step 2: Exploring the app](step-2-exploring-the-app.md)
* [Step 3: Local Development Experience with Microcks](step-3-local-development-experience.md)
* [Step 4: Write Tests for REST](step-4-write-rest-tests.md)
* [Step 5: Write Tests for Async](step-5-write-async-tests.md)

```sh
$ go run cmd/main.go
==== OUTPUT ====
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|1725661943.865|CONFWARN|rdkafka#producer-2| [thrd:app]: Configuration property group.id is a consumer property and will be ignored by this producer instance
%4|1725661943.866|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

Consumed event from topic OrderEventsAPI-0.1.0-orders-reviewed: key = 1725661947312 value = {"timestamp":1706087114133,"order":{"id":"123-456-789","customerId":"lbroudoux","status":"VALIDATED","productQuantities":[{"productName":"Croissant","quantity":1},{"productName":"Pain Chocolat","quantity":1}],"totalPrice":4.2},"changeReason":"Validation"}
Order '123-456-789' has been updated after review
[...]
```
## License Summary

In a third terminal, call the API:
The code in this repository is made available under the MIT license. See the [LICENSE](LICENSE) file for details.

_Successful call_

```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"
}
```

_Error call_

```sh
$ curl -XPOST localhost:9000/api/orders -H 'Content-Type: application/json' \
-d '{"customerId": "lbroudoux", "productQuantities": [{"productName": "Eclair Chocolat", "quantity": 1}], "totalPrice": 5.1}' -s | jq .
==== OUTPUT ====
{
"productName": "Eclair Chocolat",
"details": "Pastry Eclair Chocolat is not available"
}
```

## Running tests

Expand Down
Binary file added assets/microcks-testcontainers-go-demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/order-service-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/order-service-ecosystem.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/test-order-event-consumer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/test-order-event-publisher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/test-order-service-api.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/test-pastry-api-client.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions step-1-getting-started.md
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)
48 changes: 48 additions & 0 deletions step-2-exploring-the-app.md
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)
129 changes: 129 additions & 0 deletions step-3-local-development-experience.md
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)

0 comments on commit 3dabb49

Please sign in to comment.