From 379428e11e2bc22a6148d73ae97357188b870bb9 Mon Sep 17 00:00:00 2001 From: Laurent Broudoux Date: Tue, 5 Mar 2024 12:14:03 +0100 Subject: [PATCH] Microcks alternative to WireMock with OpenAPI contract test Signed-off-by: Laurent Broudoux --- pom.xml | 8 +- src/main/resources/catalog-openapi.yaml | 96 +++++++++++++++ step-1-getting-started.md | 2 +- step-2-exploring-the-app.md | 2 +- step-3-local-development-environment.md | 153 ++++++++++++++---------- step-5-write-tests.md | 74 +++++++++++- 6 files changed, 259 insertions(+), 76 deletions(-) create mode 100644 src/main/resources/catalog-openapi.yaml diff --git a/pom.xml b/pom.xml index 9e63b2f..cdf7a82 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 17 3.1.0 1.3.2 - 1.0-alpha-13 + 0.2.4 2.41.1 @@ -125,9 +125,9 @@ test - org.wiremock.integrations.testcontainers - wiremock-testcontainers-module - ${wiremock-testcontainers-module.version} + io.github.microcks + microcks-testcontainers + ${microcks-testcontainers-module.version} test diff --git a/src/main/resources/catalog-openapi.yaml b/src/main/resources/catalog-openapi.yaml new file mode 100644 index 0000000..30809bc --- /dev/null +++ b/src/main/resources/catalog-openapi.yaml @@ -0,0 +1,96 @@ +openapi: 3.0.2 +info: + title: Catalog Service + version: 1.0 + description: API definition of Catalog Service + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /api/products/{code}: + get: + parameters: + - name: code + description: product code + schema: + type: string + in: path + required: true + examples: + P101: + value: P101 + P102: + value: P102 + P103: + value: P103 + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + examples: + P101: + value: + id: 1 + code: P101 + name: Product P101 + description: Product P101 description + imageUrl: null + price: 34.0 + available: true + P102: + value: + id: 1 + code: P101 + name: Product P102 + description: Product P102 description + imageUrl: null + price: 25.0 + available: true + P103: + value: + id: 3 + code: P103 + name: Product P103 + description: Product P103 description + imageUrl: null + price: 15.0 + available: false +components: + schemas: + Product: + title: Root Type for catalog Product + type: object + properties: + id: + description: Unique identifier of this product + type: number + code: + description: Code of this product + type: string + name: + description: Name of this product + type: string + description: + description: Description of this product + type: string + imageUrl: + description: Url of image of this product + type: string + nullable: true + price: + description: Price of this product + type: number + available: + description: Availability of this product + type: boolean + required: + - id + - code + - name + - description + - price + - imageUrl + - available + additionalProperties: false \ No newline at end of file diff --git a/step-1-getting-started.md b/step-1-getting-started.md index 9c2b99c..9120542 100644 --- a/step-1-getting-started.md +++ b/step-1-getting-started.md @@ -74,7 +74,7 @@ This might be helpful if the internet connection at the workshop venue is somewh ```shell docker pull postgres:16-alpine docker pull localstack/localstack:2.3 -docker pull wiremock/wiremock:3.2.0-alpine +docker pull quay.io/microcks/microcks-uber:1.8.1 docker pull confluentinc/cp-kafka:7.5.0 docker pull confluentinc/cp-schema-registry:7.5.0 docker pull confluentinc/cp-enterprise-control-center:7.5.0 diff --git a/step-2-exploring-the-app.md b/step-2-exploring-the-app.md index f32946e..2270aca 100644 --- a/step-2-exploring-the-app.md +++ b/step-2-exploring-the-app.md @@ -30,7 +30,7 @@ and `com.testcontainers.catalog.events.ProductEventListener`. ## External Service Integrations Our application talks to `inventory-service` to fetch the product availability information. -We will use [WireMock](https://wiremock.org/) to mock the `inventory-service` during local development and testing. +We will use [Microcks](https://microcks.io/) to mock the `inventory-service` during local development and testing. ## API Endpoints diff --git a/step-3-local-development-environment.md b/step-3-local-development-environment.md index 2e7ad9a..6f3cb86 100644 --- a/step-3-local-development-environment.md +++ b/step-3-local-development-environment.md @@ -238,79 +238,96 @@ When we invoke the `GET /api/products/{code}` API endpoint, the application tried to call the inventory service to get the inventory details. As the inventory service is not running, we get the above error. -Let's use WireMock to mock the inventory service APIs for our local development and testing. +Let's use Microcks to mock the inventory service APIs for our local development and testing. -## Configure WireMock +## Configure Microcks Add the following dependency to your `pom.xml`: ```xml - org.wiremock.integrations.testcontainers - wiremock-testcontainers-module - 1.0-alpha-13 + io.github.microcks + microcks-testcontainers + 0.2.4 test ``` -Create `src/test/resources/mocks-config.json` to define Mock API behaviour. - -```json -{ - "mappings": [ - { - "request": { - "method": "GET", - "urlPattern": "/api/inventory/P101" - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "jsonBody": { - "code": "P101", - "quantity": 25 - } - } - }, - { - "request": { - "method": "GET", - "urlPattern": "/api/inventory/P102" - }, - "response": { - "status": 500, - "headers": { - "Content-Type": "application/json" - } - } - }, - { - "request": { - "method": "GET", - "urlPattern": "/api/inventory/P103" - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "jsonBody": { - "code": "P103", - "quantity": 0 - } - } - } - ] -} +Imagine you got an OpenAPI definition for the inventory service from the service provider. +Create `src/test/resources/inventory-openapi.yaml` with this content; this will define the Mocks behaviour: + +```yaml +openapi: 3.0.2 +info: + title: Inventory Service + version: 1.0 + description: API definition of Inventory Service + license: + name: MIT License + url: https://opensource.org/licenses/MIT +paths: + /api/inventory/{code}: + get: + parameters: + - name: code + description: product code + schema: + type: string + in: path + required: true + examples: + P101: + value: P101 + P102: + value: P102 + P103: + value: P103 + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + examples: + P101: + value: + code: P101 + quantity: 25 + P103: + value: + code: P103 + quantity: 0 + "500": + content: + application/json: + schema: + type: string + examples: + P102: + value: "" +components: + schemas: + Product: + title: Root Type for Product + type: object + properties: + code: + description: Code of this product + type: string + quantity: + description: Remaining quantity for this product + type: number + required: + - code + - quantity + additionalProperties: false ``` -Next, update the `ContainersConfig` class to configure the `WireMockContainer` as follows: +Next, update the `ContainersConfig` class to configure the `MicrocksContainer` as follows: ```java package com.testcontainers.catalog; -import org.wiremock.integrations.testcontainers.WireMockContainer; +import io.github.microcks.testcontainers.MicrocksContainer; @TestConfiguration(proxyBeanMethods = false) public class ContainersConfig { @@ -318,17 +335,21 @@ public class ContainersConfig { // [...] @Bean - WireMockContainer wiremockServer(DynamicPropertyRegistry registry) { - WireMockContainer wiremockServer = new WireMockContainer("wiremock/wiremock:3.2.0-alpine") - .withMappingFromResource("mocks-config.json"); - registry.add("application.inventory-service-url", wiremockServer::getBaseUrl); - return wiremockServer; + MicrocksContainer microcksContainer(DynamicPropertyRegistry registry) { + MicrocksContainer microcks = new MicrocksContainer("quay.io/microcks/microcks-uber:1.8.1") + .withMainArtifacts("inventory-openapi.yaml") + .withAccessToHost(true); + + registry.add( + "application.inventory-service-url", () -> microcks.getRestMockEndpoint("Inventory Service", "1.0")); + + return microcks; } } ``` -Once the WireMock server is started, we are registering the WireMock server URL as `application.inventory-service-url`. -So, when we make a call to `inventory-service` from our application, it will call the WireMock server instead. +Once the Microcks server is started, we are registering the Microcks provided mock endpoint as `application.inventory-service-url`. +So, when we make a call to `inventory-service` from our application, it will call the Microcks endpoint instead. Now restart the `TestApplication` and invoke the `GET /api/products/P101` API again. @@ -367,7 +388,7 @@ You should get the following response with `"available":false` because we mocked } ``` -Now we have a working local development environment with PostgreSQL, Kafka, LocalStack, and WireMock. +Now we have a working local development environment with PostgreSQL, Kafka, LocalStack, and Microcks. ### [Next](step-4-connect-to-services.md) diff --git a/step-5-write-tests.md b/step-5-write-tests.md index 826b9af..cac22be 100644 --- a/step-5-write-tests.md +++ b/step-5-write-tests.md @@ -5,7 +5,7 @@ But there is nothing more painful than working on a codebase without a comprehen Let's fix that!! ## Common Test SetUp -For all the integration tests in our application, we need to start PostgreSQL, Kafka, LocalStack and WireMock containers. +For all the integration tests in our application, we need to start PostgreSQL, Kafka, LocalStack and Microcks containers. So, let's create a `BaseIntegrationTest` class under `src/test/java` with the common setup as follows: ```java @@ -19,7 +19,7 @@ import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Import; -import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.Testcontainers; @SpringBootTest( webEnvironment = RANDOM_PORT, @@ -35,6 +35,7 @@ public abstract class BaseIntegrationTest { @BeforeEach void setUpBase() { RestAssured.port = port; + Testcontainers.exposeHostPorts(port); } } ``` @@ -209,9 +210,74 @@ class ProductControllerTest extends BaseIntegrationTest { } ``` +Checking product information like this is easy but become really cumbersome when the number of properties is growing +or when the `Product` class is shared among many different operations of your API. You have to check the properties +presence but also their type and this can result in sprawling code! + +If you're using an "API design-first approach", the conformance of your data structure can be automatically checked by +Microcks for you! Check the `src/main/resources/catalog-openapi.yaml` file that describes our Catalog API. + +Now let's create a test that uses Microcks to automatically check that our `ProductController` is conformance to this definition: + +```java +import io.github.microcks.testcontainers.MicrocksContainer; +import io.github.microcks.testcontainers.model.TestRequest; +import io.github.microcks.testcontainers.model.TestResult; +import io.github.microcks.testcontainers.model.TestRunnerType; +import io.restassured.RestAssured; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.test.context.jdbc.Sql; + +@Sql("/test-data.sql") +class ProductControllerTest extends BaseIntegrationTest { + @Autowired + MicrocksContainer microcks; + + @Test + void checkOpenAPIConformance() throws Exception { + microcks.importAsMainArtifact(new ClassPathResource("catalog-openapi.yaml").getFile()); + + TestRequest testRequest = new TestRequest.Builder() + .serviceId("Catalog Service:1.0") + .runnerType(TestRunnerType.OPEN_API_SCHEMA.name()) + .testEndpoint("http://host.testcontainers.internal:" + RestAssured.port) + .build(); + + TestResult testResult = microcks.testEndpoint(testRequest); + + assertThat(testResult.isSuccess()).isTrue(); + } +} +``` + +Let's understand what's going on behind the scenes: +* We complete the Microcks container with our additional `catalog-openapi.yaml` artifact file (this could have also +been done within the `ContainersConfig` class at bean initialisation). +* We prepare a `TestRequest` object that allows to specify the scope of the conformance test. Here we want to check the +conformance of `Catalog Service` with version `1.0` that are the identifier found in `catalog-openapi.yaml`. +* We ask Microcks to validate the `OpenAPI Schema` conformance by specifying a `runnerType`. +* 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). + +Finally, we're retrieving a `TestResult` from Microcks containers, and we can assert stuffs on this result, checking it's a success. + +During the test, Microcks has reused all the examples found in the `catalog-openapi.yaml` file to issue requests to +our running application. It also checked that all the received responses conform to the OpenAPI definition elements: +return codes, headers, content-type and JSON schema structure. + +If you want to get more details on the test done by Microcks, you can add those lines just before the `assertThat()`: + +```java + // You may inspect complete response object with following: + ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL); + System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(testResult)); +``` + ## Assignment * Write tests for create product API fails if the payload is invalid. * Write tests for create product API fails if the product code already exists. * Write tests for get product by code API fails if the product code does not exist. -* Write tests for get product by code API that returns `"available": false` when WireMock server return quantity=0. -* Write tests for get product by code API that returns `"available": true` from WireMock server throws Exception. +* Write tests for get product by code API that returns `"available": false` when Microcks server return quantity=0. +* Write tests for get product by code API that returns `"available": true` from Microcks server throws Exception.