Contract Testing Archives - Piotr's TechBlog https://piotrminkowski.com/tag/contract-testing/ Java, Spring, Kotlin, microservices, Kubernetes, containers Fri, 19 Apr 2024 09:42:40 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.1 https://i0.wp.com/piotrminkowski.com/wp-content/uploads/2020/08/cropped-me-2-tr-x-1.png?fit=32%2C32&ssl=1 Contract Testing Archives - Piotr's TechBlog https://piotrminkowski.com/tag/contract-testing/ 32 32 181738725 Pact with Quarkus 3 https://piotrminkowski.com/2024/04/19/pact-with-quarkus-3/ https://piotrminkowski.com/2024/04/19/pact-with-quarkus-3/#respond Fri, 19 Apr 2024 09:42:36 +0000 https://piotrminkowski.com/?p=15216 This article will teach you how to write contract tests with Pact for the app built on top of version 3 of the Quarkus framework. It is an update to the previously described topic in the “Contract Testing with Quarkus and Pact” article. Therefore we will not focus on the details related to the integration […]

The post Pact with Quarkus 3 appeared first on Piotr's TechBlog.

]]>
This article will teach you how to write contract tests with Pact for the app built on top of version 3 of the Quarkus framework. It is an update to the previously described topic in the “Contract Testing with Quarkus and Pact” article. Therefore we will not focus on the details related to the integration between Pact and Quarkus, but rather on the migration from version 2 to 3 of the Quarkus framework. There are some issues worth discussing.

You can find several other articles about Quarkus on my blog. For example, you can read about advanced testing techniques with Quarkus here. There is also an interesting article about Quarkus the Testcontainer’s support in the local development with Kafka.

Source Code

If you would like to try it by yourself, you may always take a look at my source code. It contains three microservices written in Quarkus. I migrated them from Quarkus 2 to 3, to the latest version of the Pact Quarkus extension, and from Java 17 to 21. In order to proceed with the exercise, you need to clone my GitHub repository. Then you should just follow my instructions.

Let’s do a quick recap before proceeding. We are implementing several contact tests with Pact to verify interactions between our three microservices: employee-service, department-service, and organization-service. We use Pact Broker to store and share contract definitions between the microservices. Here’s the diagram that illustrates the described architecture

pact-quarkus-3-arch

Update to Java 21

There are no issues with migration to Java 21 in Quarkus. We need to change the version of Java used in the Maven compilation inside the pom.xml file. However, the situation is more complicated with the CircleCI build. Firstly, we use the ubuntu-2204 machine in the builds to access the Docker daemon. We need Docker to run the container with the Pact broker. Although CircleCI provides the image for OpenJDK 21, there is still Java 17 installed on the latest version of ubuntu-2204. This situation will probably change during the next months. But now, we need to install OpenJDK 21 on that machine. After that, we may run Pact broker and JUnit tests using the latest Java LTS version. Here’s the CircleCI config.yaml file:

version: 2.1

jobs:
  analyze:
    executor:
      name: docker/machine
      image: ubuntu-2204:2024.01.2
    steps:
      - checkout
      - run:
          name: Install OpenJDK 21
          command: |
            java -version
            sudo apt-get update && sudo apt-get install openjdk-21-jdk
            sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java
            sudo update-alternatives --set javac /usr/lib/jvm/java-21-openjdk-amd64/bin/javac
            java -version
            export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
      - docker/install-docker-compose
      - maven/with_cache:
          steps:
            - run:
                name: Build Images
                command: mvn package -DskipTests -Dquarkus.container-image.build=true
      - run:
          name: Run Pact Broker
          command: docker-compose up -d
      - maven/with_cache:
          steps:
            - run:
                name: Run Tests
                command: mvn package pact:publish -Dquarkus.container-image.build=false
      - maven/with_cache:
          steps:
            - run:
                name: Sonar Analysis
                command: mvn package sonar:sonar -DskipTests -Dquarkus.container-image.build=false


orbs:
  maven: circleci/maven@1.4.1
  docker: circleci/docker@2.6.0

workflows:
  maven_test:
    jobs:
      - analyze:
          context: SonarCloud
YAML

Here’s the root Maven pom.xml. It declares the Maven plugin responsible for publishing contracts to the Pact broker. Each time the Pact JUnit is executed successfully, it tries to publish the JSON pacts to the broker. The ordering of Maven modules is not random. The organization-service generates and publishes pacts for verifying contracts with department-service and employee-service, so it has to be run at the beginning. As you see, we use the current latest version of Quarkus – 3.9.3.

<properties>
  <java.version>21</java.version>
  <surefire-plugin.version>3.2.5</surefire-plugin.version>
  <quarkus.version>3.9.3</quarkus.version>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <maven.compiler.source>${java.version}</maven.compiler.source>
  <maven.compiler.target>${java.version}</maven.compiler.target>
</properties>

<modules>
  <module>organization-service</module>
  <module>department-service</module>
  <module>employee-service</module>
</modules>

<build>
  <plugins>
    <plugin>
      <groupId>au.com.dius.pact.provider</groupId>
      <artifactId>maven</artifactId>
        <version>4.6.9</version>
      <configuration>
        <pactBrokerUrl>http://localhost:9292</pactBrokerUrl>
      </configuration>
    </plugin>
  </plugins>
</build>
XML

Here’s the part of the docker-compose.yml responsible for running a Pact broker. It requires a Postgres database.

version: "3.7"
services:
  postgres:
    container_name: postgres
    image: postgres
    environment:
      POSTGRES_USER: pact
      POSTGRES_PASSWORD: pact123
      POSTGRES_DB: pact
    ports:
      - "5432"
  pact-broker:
    container_name: pact-broker
    image: pactfoundation/pact-broker
    ports:
      - "9292:9292"
    depends_on:
      - postgres
    links:
      - postgres
    environment:
      PACT_BROKER_DATABASE_USERNAME: pact
      PACT_BROKER_DATABASE_PASSWORD: pact123
      PACT_BROKER_DATABASE_HOST: postgres
      PACT_BROKER_DATABASE_NAME: pact
YAML

Update Quarkus and Pact

Dependencies

Firstly, let’s take a look at the list of dependencies. With the latest versions of Quarkus, we should take care of the REST provider and client used in our app. For example, if we use the quarkus-resteasy-jackson module to expose REST services, we should also use the quarkus-resteasy-client-jackson module to call the services. On the other hand, if we use quarkus-rest-jackson on the server side, we should also use quarkus-rest-client-jackson on the client side. In order to implement Pact tests in our app, we need to include the quarkus-pact-consumer module for the contract consumer and the quarkus-pact-provider on the contract provider side. Finally, we will use Wiremock to replace a Pact mock server.

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-client-jackson</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-junit5</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-junit5-mockito</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.rest-assured</groupId>
  <artifactId>rest-assured</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.quarkiverse.pact</groupId>
  <artifactId>quarkus-pact-consumer</artifactId>
  <version>1.3.0</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>io.quarkiverse.pact</groupId>
  <artifactId>quarkus-pact-provider</artifactId>
  <version>1.3.0</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>com.github.tomakehurst</groupId>
  <artifactId>wiremock-jre8</artifactId>
  <version>3.0.1</version>
  <scope>test</scope>
</dependency>
XML

Tests Implementation with Quarkus 3 and Pact Consumer

In that exercise, I’m simplifying tests as much as possible. Therefore we will use the REST client directly to verify the contract on the consumer side. However, if you are looking for more advanced examples please go to that repository. Coming back to our exercise, let’s take a look at the example of a declarative REST client used in the department-service.

@ApplicationScoped
@Path("/employees")
@RegisterRestClient(configKey = "employee")
public interface EmployeeClient {

    @GET
    @Path("/department/{departmentId}")
    @Produces(MediaType.APPLICATION_JSON)
    List<Employee> findByDepartment(@PathParam("departmentId") Long departmentId);

}
Java

There are some significant changes in the Pact tests on the consumer side. Some of them were forced by the error related to migration to Quarkus 3 described here. I found a smart workaround for that problem proposed by one of the contributors (1). This workaround replaces the Pact built-in mock server with Wiremock. We will start Wiremock on the dynamic port (2). We also need to implement @QuarkusTestResource to start the Wiremock container before the tests and shut it down after the tests (3). Then, we can switch to the latest version of Pact API by returning the V4Pact object (4) in the @Pact method and updating the @PactTestFor annotation accordingly (5). Finally, instead of the Pact MockServer, we use the wrapper PactMockServer dedicated to Wiremock (6).

@QuarkusTest
@ExtendWith(PactConsumerTestExt.class)
@ExtendWith(PactMockServerWorkaround.class) // (1)
@MockServerConfig(port = "0") // (2)
@QuarkusTestResource(WireMockQuarkusTestResource.class) // (3)
public class EmployeeClientContractTests extends PactConsumerTestBase {

    @Pact(provider = "employee-service", consumer = "department-service")
    public V4Pact callFindDepartment(PactDslWithProvider builder) { // (4)
        DslPart body = PactDslJsonArray.arrayEachLike()
                .integerType("id")
                .stringType("name")
                .stringType("position")
                .numberType("age")
                .closeObject();
        return builder.given("findByDepartment")
                .uponReceiving("findByDepartment")
                    .path("/employees/department/1")
                    .method("GET")
                .willRespondWith()
                    .status(200)
                    .body(body).toPact(V4Pact.class);
    }

    @Test
    // (5)
    @PactTestFor(providerName = "employee-service", pactVersion = PactSpecVersion.V4)
    public void verifyFindDepartmentPact(final PactMockServer mockServer) { // (6)
        EmployeeClient client = RestClientBuilder.newBuilder()
                .baseUri(URI.create(mockServer.getUrl()))
                .build(EmployeeClient.class);
        List<Employee> employees = client.findByDepartment(1L);
        System.out.println(employees);
        assertNotNull(employees);
        assertTrue(employees.size() > 0);
        assertNotNull(employees.get(0).getId());
    }
}
Java

Here’s our PactMockServer wrapper:

public class PactMockServer {

    private final String url;
    private final int port;

    public PactMockServer(String url, int port) {
        this.url = url;
        this.port = port;
    }

    public String getUrl() {
        return url;
    }

    public int getPort() {
        return port;
    }
}
Java

Implement Mock Server with Wiremock

In the first step, we need to provide an implementation of the QuarkusTestResourceLifecycleManager for starting the Wiremock server during the tests.

public class WireMockQuarkusTestResource implements 
        QuarkusTestResourceLifecycleManager {
        
    private static final Logger LOGGER = Logger
       .getLogger(WireMockQuarkusTestResource.class);

    private WireMockServer wireMockServer;

    @Override
    public Map<String, String> start() {
        final HashMap<String, String> result = new HashMap<>();

        this.wireMockServer = new WireMockServer(options()
                .dynamicPort()
                .notifier(createNotifier(true)));
        this.wireMockServer.start();

        return result;
    }

    @Override
    public void stop() {
        if (this.wireMockServer != null) {
            this.wireMockServer.stop();
            this.wireMockServer = null;
        }
    }

    @Override
    public void inject(final TestInjector testInjector) {
        testInjector.injectIntoFields(wireMockServer,
          new TestInjector.AnnotatedAndMatchesType(InjectWireMock.class, 
                                                   WireMockServer.class));
    }

    private static Notifier createNotifier(final boolean verbose) {
        final String prefix = "[WireMock] ";
        return new Notifier() {

            @Override
            public void info(final String s) {
                if (verbose) {
                    LOGGER.info(prefix + s);
                }
            }

            @Override
            public void error(final String s) {
                LOGGER.warn(prefix + s);
            }

            @Override
            public void error(final String s, final Throwable throwable) {
                LOGGER.warn(prefix + s, throwable);
            }
        };
    }
}
Java

Let’s create the annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectWireMock {
}
Java

I’m not very sure it is required. But here’s the test base extended by our tests.

public class PactConsumerTestBase {

   @InjectWireMock
   protected WireMockServer wiremock;

   @BeforeEach
   void initWiremockBeforeEach() {
      wiremock.resetAll();
      configureFor(new WireMock(this.wiremock));
   }

   protected void forwardToPactServer(final PactMockServer wrapper) {
      wiremock.resetAll();  
      stubFor(any(anyUrl())
         .atPriority(1)
         .willReturn(aResponse().proxiedFrom(wrapper.getUrl()))
      );
   }

}
Java

Here’s the workaround implementation used as the test extension included with the @ExtendWith annotation:

public class PactMockServerWorkaround implements ParameterResolver {
    
  @Override
  public boolean supportsParameter(ParameterContext parameterContext, 
                                   ExtensionContext extensionContext)
      throws ParameterResolutionException {

     return parameterContext.getParameter().getType() == PactMockServer.class;
  }

  @Override
  @SuppressWarnings("unchecked")
  public Object resolveParameter(ParameterContext parameterContext, 
                                 ExtensionContext extensionContext)
      throws ParameterResolutionException {

      final ExtensionContext.Store store = extensionContext
           .getStore(ExtensionContext.Namespace.create("pact-jvm"));

      if (store.get("providers") == null) {
         return null;
      }

      final List<Pair<ProviderInfo, List<String>>> providers = store
         .get("providers", List.class);
      var pair = providers.get(0);
      final ProviderInfo providerInfo = pair.getFirst();

      var mockServer = store.get("mockServer:" + providerInfo.getProviderName(),
                MockServer.class);

      return new PactMockServer(mockServer.getUrl(), mockServer.getPort());
   }
}
Java

I intentionally do not comment on this workaround. Maybe it could be somehow improved. I wish that everything would work fine just after migrating the Pact extension to Quarkus 3 without any workarounds. However, thanks to the workaround, I was able to run my Pact tests successfully and then update all the required dependencies to the latest.

Final Thoughts

This article guides you through the changes required to migrate your microservices and Pact contract tests from Qaurkus 2 to 3. For me, it is important to automatically update all the dependencies in my demo projects to be up-to-date as described here. I’m using Renovate to automatically scan and update Maven pom.xml dependencies. Once it updates the version of the dependency it runs all the JUnit tests for the verification. The process is automatically performed on the CircleCI. You can view the history of builds of the sample repository used in that article.

The post Pact with Quarkus 3 appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2024/04/19/pact-with-quarkus-3/feed/ 0 15216
Contract Testing on Kubernetes with Microcks https://piotrminkowski.com/2023/05/20/contract-testing-on-kubernetes-with-microcks/ https://piotrminkowski.com/2023/05/20/contract-testing-on-kubernetes-with-microcks/#comments Sat, 20 May 2023 08:09:32 +0000 https://piotrminkowski.com/?p=14182 This article will teach you how to design and perform contract testing on Kubernetes with Microcks. Microcks is a Kubernetes native tool for API mocking and testing. It supports several specifications including OpenAPI, AsyncAPI, a GraphQL schema, or a gRPC/Protobuf schema. In contrast to the other tools described in my blog before (Pact, Spring Cloud Contract) it performs […]

The post Contract Testing on Kubernetes with Microcks appeared first on Piotr's TechBlog.

]]>
This article will teach you how to design and perform contract testing on Kubernetes with Microcks. Microcks is a Kubernetes native tool for API mocking and testing. It supports several specifications including OpenAPI, AsyncAPI, a GraphQL schema, or a gRPC/Protobuf schema. In contrast to the other tools described in my blog before (Pact, Spring Cloud Contract) it performs a provider-driven contract testing. Moreover, Microcks runs tests against real endpoints and verifies them with the defined schema. In our case, we will deploy the Quarkus microservices on Kubernetes and then perform some contract tests using Microcks. Let’s begin.

If you want to compare Microcks with other testing tools, you may be interested in my article about contract testing with Quarkus and Pact available here.

Source Code

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. Then you should just follow my instructions 🙂

Install Microcks on Kubernetes

We can install Microcks on Kubernetes with Helm chart or with the operator. Since I’m using OpenShift as a Kubernetes platform in this exercise, the simplest way is through the operator. Assuming we have already installed the OLM (Operator Lifecycle Manager) on Kubernetes we need to apply the following YAML manifest:

apiVersion: v1
kind: Namespace
metadata:
  name: microcks
---
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
  name: operatorgroup
  namespace: microcks
spec:
  targetNamespaces:
  - microcks
---
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: my-microcks
  namespace: microcks
spec:
  channel: stable
  name: microcks
  source: operatorhubio-catalog
  sourceNamespace: olm

If you want to easily manage operators on Kubernetes you need to install Operator Lifecycle Manager first. Here are the installation instructions: https://olm.operatorframework.io/docs/getting-started/. If you use OpenShift you don’t have to install anything.

With OpenShift we can install the Microcks operator using the UI dashboard. Once we do it, we need to create the object responsible for Microcks installation. In the OpenShift console, we need to click the “MicrocksInstall” link as shown below.

Microcks requires some additional staff like Keycloak or MongoDB to be installed on the cluster. Here’s the YAML manifest responsible for installation on Kubernetes.

apiVersion: microcks.github.io/v1alpha1
kind: MicrocksInstall
metadata:
  name: my-microcksinstall
  namespace: microcks
spec:
  name: my-microcksinstall
  version: 1.7.0
  microcks:
    replicas: 1
  postman:
    replicas: 1
  keycloak:
    install: true
    persistent: true
    volumeSize: 1Gi
  mongodb:
    install: true
    persistent: true
    volumeSize: 2Gi

By applying the YAML manifest with the MicrocksInstall object, we will start the installation process. We should have the following list of running pods if it finished successfully:

$ kubectl get pod -n microcks
NAME                                                     READY   STATUS    RESTARTS   AGE
microcks-ansible-operator-56ddbdcccf-6lrdl               1/1     Running   0          4h14m
my-microcksinstall-76b67f4f77-jdcj7                      1/1     Running   0          4h13m
my-microcksinstall-keycloak-79c5f68f45-5nm69             1/1     Running   0          4h13m
my-microcksinstall-keycloak-postgresql-97f69c476-km6p2   1/1     Running   0          4h13m
my-microcksinstall-mongodb-846f7c7976-tl5jh              1/1     Running   0          4h13m
my-microcksinstall-postman-runtime-574d4bf7dc-xfct7      1/1     Running   0          4h13m

In order to prepare the contract testing process on Kubernetes with Microcks we need to access it dashboard. It is exposed by the my-microcksinstall service under the 8080 port. I’m accessing it through the OpenShift Route object. If you use Kubernetes you can enable port forwarding (the kubectl port-forward command) for that service or expose it through the Ingress object.

contract-testing-kubernetes-microcks-svc

Here’s the Microcks UI dashboard. In the first step, we need to import our API documentation and samples by clicking the Importers button. Before we do it, we need to prepare the document in one of the supported specifications. In our case, it is the OpenAPI specification.

Create the Provider Side App

We create a simple app with Quarkus that exposes some REST endpoints. In order to access it go to the employee-service directory in our sample Git repository. Here’s the controller class responsible for the endpoints implementation.

@Path("/employees")
@Produces(MediaType.APPLICATION_JSON)
public class EmployeeController {

   private static final Logger LOGGER = LoggerFactory
      .getLogger(EmployeeController.class);

   @Inject
   EmployeeRepository repository;

   @POST
   public Employee add(@Valid Employee employee) {
      LOGGER.info("Employee add: {}", employee);
      return repository.add(employee);
   }

   @Path("/{id}")
   @GET
   public Employee findById(@PathParam("id") Long id) {
      LOGGER.info("Employee find: id={}", id);
      return repository.findById(id);
   }

   @GET
   public Set<Employee> findAll() {
      LOGGER.info("Employee find");
      return repository.findAll();
   }

   @Path("/department/{departmentId}")
   @GET
   public Set<Employee> findByDepartment(@PathParam("departmentId") Long departmentId) {
      LOGGER.info("Employee find: departmentId={}", departmentId);
      return repository.findByDepartment(departmentId);
   }

   @Path("/organization/{organizationId}")
   @GET
   public Set<Employee> findByOrganization(@PathParam("organizationId") Long organizationId) {
      LOGGER.info("Employee find: organizationId={}", organizationId);
      return repository.findByOrganization(organizationId);
   }

}

If we add the module responsible for generating and exposing OpenAPI documentation we can easily access it under the /q/openapi path. In order to achieve to include the following Maven dependency:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>

Here’s the OpenAPI descriptor automatically generated by Quarkus for our employee-service app.

openapi: 3.0.3
info:
  title: employee-service API
  version: "1.2"
paths:
  /employees:
    get:
      tags:
      - Employee Controller
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                uniqueItems: true
                type: array
                items:
                  $ref: '#/components/schemas/Employee'
    post:
      tags:
      - Employee Controller
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Employee'
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Employee'
  /employees/department/{departmentId}:
    get:
      tags:
      - Employee Controller
      parameters:
      - name: departmentId
        in: path
        required: true
        schema:
          format: int64
          type: integer
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                uniqueItems: true
                type: array
                items:
                  $ref: '#/components/schemas/Employee'
  /employees/organization/{organizationId}:
    get:
      tags:
      - Employee Controller
      parameters:
      - name: organizationId
        in: path
        required: true
        schema:
          format: int64
          type: integer
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                uniqueItems: true
                type: array
                items:
                  $ref: '#/components/schemas/Employee'
  /employees/{id}:
    get:
      tags:
      - Employee Controller
      parameters:
      - name: id
        in: path
        required: true
        schema:
          format: int64
          type: integer
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Employee'
components:
  schemas:
    Employee:
      required:
      - organizationId
      - departmentId
      - name
      - position
      type: object
      properties:
        id:
          format: int64
          type: integer
        organizationId:
          format: int64
          type: integer
        departmentId:
          format: int64
          type: integer
        name:
          pattern: \S
          type: string
        age:
          format: int32
          maximum: 100
          minimum: 1
          type: integer
        position:
          pattern: \S
          type: string

We need to add the examples section to each responses and parameters section. Let’s begin with the GET /employees endpoint. It returns all existing employees. The name of the example is not important. You can set any name you want. As the returned value we set the JSON array with three Employee objects.

/employees:
  get:
    tags:
      - Employee Controller
    responses:
      "200":
        description: OK
        content:
          application/json:
            schema:
              uniqueItems: true
              type: array
              items:
                $ref: '#/components/schemas/Employee'
            examples:
              all_persons:
                value: |-
                  [
                     {"id": 1, "name": "Test User 1", "age": 20, "organizationId": 1, "departmentId": 1, "position": "developer"},
                     {"id": 2, "name": "Test User 2", "age": 30, "organizationId": 1, "departmentId": 2, "position": "architect"},
                     {"id": 3, "name": "Test User 3", "age": 40, "organizationId": 2, "departmentId": 3, "position": "developer"},
                  ]

For comparison, let’s take a look at the OpenAPI docs for the POST /employees endpoint. It returns a single JSON object as a response. We also had to add examples in the requestBody section.

/employees:
  post:
    tags:
      - Employee Controller
    requestBody:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Employee'
          examples:
            add_employee:
              summary: Hire a new employee
              description: Should return 200
              value: '{"name": "Test User 4", "age": 50, "organizationId": 2, "departmentId": 3, "position": "tester"}'
    responses:
      "200":
        description: OK
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Employee'
            examples:
              add_employee:
                value: |-
                  {"id": 4, "name": "Test User 4", "age": 50, "organizationId": 2, "departmentId": 3, "position": "tester"}

Finally, we can proceed to the endpoint GET /employees/department/{departmentId}, which returns all the employees assigned to the particular department. This endpoint is called by another app – department-service. We need to add the example value of the path variable referring to the id of a department. The same as before, we also have to include an example JSON in the responses section.

/employees/department/{departmentId}:
  get:
    tags:
      - Employee Controller
    parameters:
      - name: departmentId
        in: path
        required: true
        schema:
          format: int64
          type: integer
        examples:
          find_by_dep_1:
            summary: Main id of department
            value: 1
    responses:
      "200":
        description: OK
        content:
          application/json:
            schema:
              uniqueItems: true
              type: array
              items:
                $ref: '#/components/schemas/Employee'
            examples:
              find_by_dep_1:
                value: |-
                  [
                    { "id": 1, "name": "Test User 1", "age": 20, "organizationId": 1, "departmentId": 1, "position": "developer" }
                  ]

Create the Consumer Side App

As I mentioned before, we have another app – department-service. It consumer endpoint exposed by the employee-service. Here’s the implementation of the Quarkus declarative REST client responsible for calling the GET /employees/department/{departmentId} endpoint:

@ApplicationScoped
@Path("/employees")
@RegisterRestClient(configKey = "employee")
public interface EmployeeClient {

    @GET
    @Path("/department/{departmentId}")
    @Produces(MediaType.APPLICATION_JSON)
    List<Employee> findByDepartment(@PathParam("departmentId") Long departmentId);

}

That client is then used by the department-service REST controller to return all the employees in the particular organization with the division into departments.

@Path("/departments")
@Produces(MediaType.APPLICATION_JSON)
public class DepartmentController {

    private static final Logger LOGGER = LoggerFactory
       .getLogger(DepartmentController.class);

    @Inject
    DepartmentRepository repository;
    @Inject
    @RestClient
    EmployeeClient employeeClient;

    // ... implementation of other endpoints

    @Path("/organization/{organizationId}/with-employees")
    @GET
    public Set<Department> findByOrganizationWithEmployees(@PathParam("organizationId") Long organizationId) {
        LOGGER.info("Department find: organizationId={}", organizationId);
        Set<Department> departments = repository.findByOrganization(organizationId);
        departments.forEach(d -> d.setEmployees(employeeClient.findByDepartment(d.getId())));
        return departments;
    }

}

Finally, we need to configure the base URI for the client in the Quarkus application.properties file. We can also set that address in the corresponding environment variable QUARKUS_REST_CLIENT_EMPLOYEE_URL.

quarkus.rest-client.employee.url = http://employee:8080

Test API with Microcks

Once we implemented our apps, we can back to the Microcks dashboard. In the Importers section import your openapi.yml file.

contract-testing-kubernetes-microcks-upload

Once you will import the OpenAPI definition you can go to the APIs | Services section. You should already have the record with your API description as shown below. The current version of our is 1.2. The employee-service exposes five REST endpoints.

We can display the details of the service by clicking on the record or on the Details button. It displays a list of available endpoints with a number of examples declared in the OpenAPI manifest.

contract-testing-kubernetes-microcks-api-samples

Let’s display the details of the GET /employees/department/{departmentId} endpoint. This endpoint is called by the department-service. Microcks creates the mock on Kubernetes for the purposes of contract testing. The mock is available inside Kubernetes and as well as outside it through the OpenShift Route. Now, our goal is to verify the compliance of our mock with the real service.

In order to verify the Microcks mock with the real service, we need to deploy our employee-service app on Kubernetes. We can easily do it using the Quarkus Kubernetes extension. Thanks to that it is possible to build the app, build the image and deploy it on Kubernetes or OpenShift using a single Maven command. Here’s the dedicated Maven profile for that. For OpenShift, we need to include the single dependency quarkus-openshift, and set the property quarkus.kubernetes.deploy to true.

<profile>
  <id>openshift</id>
  <activation>
    <property>
      <name>openshift</name>
    </property>
  </activation>
  <dependencies>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-openshift</artifactId>
    </dependency>
  </dependencies>
  <properties>
    <quarkus.kubernetes.deploy>true</quarkus.kubernetes.deploy>
    <quarkus.profile>openshift</quarkus.profile>
    <quarkus.container-image.builder>openshift</quarkus.container-image.builder>
  </properties>
</profile>

Then, we just need to activate the openshift profile during the Maven build. Go to the employee-service directory and execute the following command:

$ mvn clean package -Popenshift

The internal address of our app is defined by the name of the Kubernetes service:

$ oc get svc
NAME               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
employee-service   ClusterIP   172.30.141.85   <none>        80/TCP    27h

Finally, we can our samples against real endpoints. We need to set the address of our service in the Test Endpoint field. Then, we choose OPEN_API_SCHEMA as the Runner. Finally, we can execute the test by clicking the Launch test button.

contract-testing-kubernetes-microcks-ui

Viewing and Analysing Test Results

Once we run the test we can display the results with the Microcks dashboard. By default, it calls all the endpoints defined in the OpenAPI manifest. We can override this behavior and choose a list of endpoints that should be tested.

We can verify the details of each test. For example, we can display a response from our employee-service running on the cluster for the GET /employees/department/{departmentId} endpoint.

We can automate the testing process with Microcks using its CLI. You can read more details about it in the docs. In order to execute the test in a similar way as we did before via the Microcks dashboard, we need to use the test command. It requires several input arguments. We need to pass the name and version of our API, the URI of the test endpoint, and the type of runner. By default, Microcks creates a client in Keycloak. The client id is microcks-serviceaccount. You need to log in to your Keycloak instance and copy the value of the client secret. As the microcksURL argument, we need to pass the external address of Microcks with the /api path. If you would run it inside the Kubernetes cluster, you could run the internal address http://my-microcksinstall.microcks.svc.cluster.local:8080/api.

$ microcks-cli test 'employee-service API:1.2' \
  http://employee-service.demo-microcks.svc.cluster.local \
  OPEN_API_SCHEMA \
  --microcksURL=https://my-microcksinstall-microcks.apps.pvxvtsz4.eastus.aroapp.io/api \
  --keycloakClientId microcks-serviceaccount \
  --keycloakClientSecret ab54d329-e435-41ae-a900-ec6b3fe15c54

Once we run a test, Microcks calculates the conformance score after finishing. It is a kind of grade that estimates how your API contract is actually covered by the samples you’ve attached to it. It is computed based on the number of samples you’ve got on each operation, and the complexity of dispatching rules of this operation. As you there is still a place for improvement in my case 🙂

Verify Contract on the Consumer Side

Let’s create a simple test that verifies the contract with employee-service in the department-service. It will call the department-service GET /departments/organization/{organizationId}/with-employees endpoint that interacts with the employee-service. We won’t mock the REST client.

@QuarkusTest
public class DepartmentExternalContractTests {

    @Test
    void findByOrganizationWithEmployees() {
        when().get("/departments/organization/{organizationId}/with-employees", 1L).then()
                .statusCode(200)
                .body("size()", notNullValue());
    }

}

Instead of mocking the client directly in the test, we use the mock endpoint exposed by Microcks. We will use the internal of this mock for the employee-service is https://my-microcksinstall.microcks.svc.cluster.local/rest/employee-service+API/1.2/employees. We can leverage the Quarkus environment variable to set the address for the client. As I mentioned before we can use the QUARKUS_REST_CLIENT_EMPLOYEE_URL environment variable.

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: maven-contract-tests
spec:
  params:
    - default: >-
        image-registry.openshift-image-registry.svc:5000/openshift/java:latest
      description: Maven base image
      name: MAVEN_IMAGE
      type: string
    - default: .
      description: >-
        The context directory within the repository for sources on which we want
        to execute maven goals.
      name: CONTEXT_DIR
      type: string
    - name: EMPLOYEE_URL
      default: http://my-microcksinstall.microcks.svc.cluster.local:8080/rest/employee-service+API/1.2
  steps:
    - image: $(params.MAVEN_IMAGE)
      name: mvn-command
      env:
        - name: QUARKUS_REST_CLIENT_EMPLOYEE_URL
          value: $(params.EMPLOYEE_URL)
      resources: {}
      script: >
        #!/usr/bin/env bash

        /usr/bin/mvn clean package -Pmicrocks

      workingDir: $(workspaces.source.path)/$(params.CONTEXT_DIR)
  workspaces:
    - name: source

As you see in the code above, we are activating the microcks profile during the Maven build. That’s because we want to run the test defined in DepartmentExternalContractTests class only if the build is performed on Kubernetes.

<profile>
  <id>microcks</id>
  <activation>
    <property>
      <name>microcks</name>
    </property>
  </activation>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>${surefire-plugin.version}</version>
        <configuration>
          <includes>
            <include>**/DepartmentExternalContractTests.java</include>
          </includes>
        </configuration>
      </plugin>
    </plugins>
  </build>
</profile>

Finally, we can define and start a pipeline that clones our Git repository and runs the test against the endpoint mocked by the Microcks. Here’s the definition of our pipeline.

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: microcks-pipeline
spec:
  tasks:
    - name: git-clone
      params:
        - name: url
          value: 'https://github.com/piomin/sample-quarkus-microservices.git'
        - name: revision
          value: master
        - name: submodules
          value: 'true'
        - name: depth
          value: '1'
        - name: sslVerify
          value: 'false'
        - name: crtFileName
          value: ca-bundle.crt
        - name: deleteExisting
          value: 'true'
        - name: verbose
          value: 'true'
        - name: gitInitImage
          value: >-
            registry.redhat.io/openshift-pipelines/pipelines-git-init-rhel8@sha256:a538c423e7a11aae6ae582a411fdb090936458075f99af4ce5add038bb6983e8
        - name: userHome
          value: /tekton/home
      taskRef:
        kind: ClusterTask
        name: git-clone
      workspaces:
        - name: output
          workspace: source-dir
    - name: maven-contract-tests
      params:
        - name: MAVEN_IMAGE
          value: >-
            image-registry.openshift-image-registry.svc:5000/openshift/java:latest
        - name: CONTEXT_DIR
          value: department-service
        - name: EMPLOYEE_URL
          value: >-
            http://my-microcksinstall.microcks.svc.cluster.local:8080/rest/employee-service+API/1.2
      runAfter:
        - git-clone
      taskRef:
        kind: Task
        name: maven-contract-tests
      workspaces:
        - name: source
          workspace: source-dir
  workspaces:
    - name: source-dir

Final Thoughts

In this article, I described just one of several possible scenarios for contract testing on Kubernetes with Microcks. You can as well use it to test interactions, for example with Kafka or with GraphQL endpoints. Microcks can be a central point for testing contracts of apps running on Kubernetes. It is able to verify contracts by calling real endpoints exposed by the apps. It provides UI for visualizing and running tests, as well as CLI for automation.

The post Contract Testing on Kubernetes with Microcks appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2023/05/20/contract-testing-on-kubernetes-with-microcks/feed/ 2 14182
Contract Testing with Quarkus and Pact https://piotrminkowski.com/2023/05/09/contract-testing-with-quarkus-and-pact/ https://piotrminkowski.com/2023/05/09/contract-testing-with-quarkus-and-pact/#respond Tue, 09 May 2023 09:57:17 +0000 https://piotrminkowski.com/?p=14166 In this article, you will learn how to create contract tests for Quarkus apps using Pact. Consumer-driven contract testing is one of the most popular strategies for verifying communication between microservices. In short, it is an approach to ensure that services can successfully communicate with each other without implementing integration tests. There are some tools […]

The post Contract Testing with Quarkus and Pact appeared first on Piotr's TechBlog.

]]>
In this article, you will learn how to create contract tests for Quarkus apps using Pact. Consumer-driven contract testing is one of the most popular strategies for verifying communication between microservices. In short, it is an approach to ensure that services can successfully communicate with each other without implementing integration tests. There are some tools especially dedicated to such a type of test. One of them is Pact. We can use this code-first tool with multiple languages including .NET, Go, Ruby, and of course, Java.

Before you start, it is worth familiarizing yourself with the Quarkus framework. There are several articles about Quarkus on my blog. If you want about to read about interesting and useful Quarkus features please refer to the post “Quarkus Tips, Tricks and Techniques” available here. For some more advanced features like testing strategies, you can read the “Advanced Testing with Quarkus” article available here.

Source Code

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. Then you should just follow my instructions. Let’s begin.

Architecture

In the exercise today we will add several contract tests to the existing architecture of sample Quarkus microservices. There are three sample apps that communicate with each other through HTTP. We use Quarkus declarative REST client to call remote HTTP endpoints. The department-service app calls the endpoint exposed by the employee-service app to get a list of employees assigned to the particular department. On the other hand, the organization-service app calls endpoints exposed by department-service and employee-service.

We will implement some contract tests to verify described interactions. Each contract is signed between two sides of communication: the consumer and the provider. Pact assumes that contract code is generated and published by the consumer side, and then verified by the provider side. It provides a tool for storing and sharing contracts between consumers and providers – Pact Broker. Pact Broker exposes a simple RESTful API for publishing and retrieving contracts, and an embedded web dashboard for navigating the API. We will run it as a Docker container. However, our goal is also to run it during the CI build and then use it to exchange contracts between the tests.

Here’s the diagram that illustrates the described architecture.

quarkus-pact-arch

Running Pact Broker

Before we create any test, we will start Pact broker on the local machine. In order to do that, we need to run two containers on Docker. Pact broker requires database, so in the first step we will start the postgres container:

$ docker run -d --name postgres \
  -p 5432:5432 \
  -e POSTGRES_USER=pact \ 
  -e POSTGRES_PASSWORD=pact123 \
  -e POSTGRES_DB=pact \
  postgres

After that, we can run the container with Pact broker. We will link it to the postgres container and set the autentication credentials:

$ docker run -d --name pact-broker \
  --link postgres:postgres \
  -e PACT_BROKER_DATABASE_USERNAME=pact \
  -e PACT_BROKER_DATABASE_PASSWORD=pact123 \ 
  -e PACT_BROKER_DATABASE_HOST=postgres \
  -e PACT_BROKER_DATABASE_NAME=pact \
  -p 9292:9292 \
  pactfoundation/pact-broker

If you prefer to run everything with the single command you can use docker-compose.yml in the repository root directory. It will run not only Postgres and Pact broker, but also our three sample microservices.

version: "3.7"
services:
  postgres:
    container_name: postgres
    image: postgres
    environment:
      POSTGRES_USER: pact
      POSTGRES_PASSWORD: pact123
      POSTGRES_DB: pact
    ports:
      - "5432"
  pact-broker:
    container_name: pact-broker
    image: pactfoundation/pact-broker
    ports:
      - "9292:9292"
    depends_on:
      - postgres
    links:
      - postgres
    environment:
      PACT_BROKER_DATABASE_USERNAME: pact
      PACT_BROKER_DATABASE_PASSWORD: pact123
      PACT_BROKER_DATABASE_HOST: postgres
      PACT_BROKER_DATABASE_NAME: pact
  employee:
    image: quarkus/employee-service:1.2
    ports:
      - "8080"
  department:
    image: quarkus/department-service:1.1
    ports:
      - "8080"
    links:
      - employee
  organization:
    image: quarkus/organization-service:1.1
    ports:
      - "8080"
    links:
      - employee
      - department

Since the docker-compose.yml includes images with our sample microservices, you first need to build the Docker images of the apps. We can easily do it with Quarkus. Once we included the quarkus-container-image-jib dependency, we may build the image using Jib Maven plugin by activating the quarkus.container-image.build property as shown below. Additionally, don’t forget about skipping the tests.

$ mvn clean package -DskipTests -Dquarkus.container-image.build=true

Then just run the following command:

$ docker compose up

Finally, you can access the Pact broker UI under the http://localhost:9292 address. Of course, there are no contracts saved there, so you just see the example pact.

Create Contract Test for Consumer

Once we started a Pact broker we can proceed to the implementation of the tests. We will start from the consumer side. Both departament-service and organization-service consuming endpoints exposed by the employee-service. In the first step, we will include the Quarkus Pact Consumer extension to the Maven dependencies.

<dependency>
  <groupId>io.quarkiverse.pact</groupId>
  <artifactId>quarkus-pact-consumer</artifactId>
  <version>1.0.0.Final</version>
  <scope>provided</scope>
</dependency>

Here’s the REST client interface responsible for calling the employee-service GET /employees/department/{id} endpoint from the departament-service.

@ApplicationScoped
@Path("/employees")
@RegisterRestClient(configKey = "employee")
public interface EmployeeClient {

    @GET
    @Path("/department/{departmentId}")
    @Produces(MediaType.APPLICATION_JSON)
    List<Employee> findByDepartment(@PathParam("departmentId") Long departmentId);

}

We will test the EmployeeClient directly in the contract test. In order to implement a contract test on the consumer side we need to declare the PactConsumerTestExt JUnit5 extension. In the callFindByDepartment method, we need to prepare the expected response template as the RequestResponsePact object. The method should return an array of employees. Therefore we are using the PactDslJsonArray to construct the required object. The name of the provider is employee-service, while the name of the consumer is department-service. In order to use Pact MockServer I had to declare v3 of Pact instead of the latest v4. Then we are setting the mock server address as the RestClientBuilder base URI and test the contract.

@QuarkusTest
@ExtendWith(PactConsumerTestExt.class)
public class EmployeeClientContractTests {

    @Pact(provider = "employee-service", 
          consumer = "department-service")
    public RequestResponsePact callFindByDepartment(
        PactDslWithProvider builder) {
        DslPart body = PactDslJsonArray.arrayEachLike()
                .integerType("id")
                .stringType("name")
                .stringType("position")
                .numberType("age")
                .closeObject();
        return builder.given("findByDepartment")
                .uponReceiving("findByDepartment")
                    .path("/employees/department/1")
                    .method("GET")
                .willRespondWith()
                    .status(200)
                    .body(body).toPact();
    }

    @Test
    @PactTestFor(providerName = "employee-service", 
                 pactVersion = PactSpecVersion.V3)
    public void verifyFindDepartmentPact(MockServer mockServer) {
        EmployeeClient client = RestClientBuilder.newBuilder()
                .baseUri(URI.create(mockServer.getUrl()))
                .build(EmployeeClient.class);
        List<Employee> employees = client.findByDepartment(1L);
        assertNotNull(employees);
        assertTrue(employees.size() > 0);
        assertNotNull(employees.get(0).getId());
    }
}

The test for the integration between organization-service and department-service is pretty similar. Let’s take a look at the REST client interface.

@ApplicationScoped
@Path("/departments")
@RegisterRestClient(configKey = "department")
public interface DepartmentClient {

    @GET
    @Path("/organization/{organizationId}")
    @Produces(MediaType.APPLICATION_JSON)
    List<Department> findByOrganization(@PathParam("organizationId") Long organizationId);

    @GET
    @Path("/organization/{organizationId}/with-employees")
    @Produces(MediaType.APPLICATION_JSON)
    List<Department> findByOrganizationWithEmployees(@PathParam("organizationId") Long organizationId);

}

Here’s the implementation of our contract test. However, instead of a single endpoint, we are testing two interactions with the GET /departments/organization/{id} and GET /departments/organization/{id}/with-employees.

@QuarkusTest
@ExtendWith(PactConsumerTestExt.class)
public class DepartmentClientContractTests {

   @Pact(provider = "department-service", 
         consumer = "organization-service")
   public RequestResponsePact callFindDepartment(
      PactDslWithProvider builder) {
      DslPart body = PactDslJsonArray.arrayEachLike()
            .integerType("id")
            .stringType("name")
            .closeObject();
      DslPart body2 = PactDslJsonArray.arrayEachLike()
            .integerType("id")
            .stringType("name")
            .array("employees")
               .object()
                  .integerType("id")
                  .stringType("name")
                  .stringType("position")
                  .integerType("age")
               .closeObject()
            .closeArray();
      return builder
            .given("findByOrganization")
               .uponReceiving("findByOrganization")
                  .path("/departments/organization/1")
                  .method("GET")
               .willRespondWith()
                  .status(200)
                  .body(body)
            .given("findByOrganizationWithEmployees")
               .uponReceiving("findByOrganizationWithEmployees")
                  .path("/departments/organization/1/with-employees")
                  .method("GET")
               .willRespondWith()
                  .status(200)
                  .body(body2)
            .toPact();
   }

   @Test
   @PactTestFor(providerName = "department-service", 
                pactVersion = PactSpecVersion.V3)
   public void verifyFindByOrganizationPact(MockServer mockServer) {
      DepartmentClient client = RestClientBuilder.newBuilder()
             .baseUri(URI.create(mockServer.getUrl()))
             .build(DepartmentClient.class);
      List<Department> departments = client.findByOrganization(1L);
      assertNotNull(departments);
      assertTrue(departments.size() > 0);
      assertNotNull(departments.get(0).getId());

      departments = client.findByOrganizationWithEmployees(1L);
      assertNotNull(departments);
      assertTrue(departments.size() > 0);
      assertNotNull(departments.get(0).getId());
      assertFalse(departments.get(0).getEmployees().isEmpty());
   }

}

Publish Contracts to the Pact Broker

That’s not all. We are still on the consumer side. After running the tests we need to publish the contract to the Pact broker. It is not performed automatically by Pact. To achieve it, we first need to include the following Maven plugin:

<plugin>
  <groupId>au.com.dius.pact.provider</groupId>
  <artifactId>maven</artifactId>
  <version>4.6.0</version>
  <configuration>
    <pactBrokerUrl>http://localhost:9292</pactBrokerUrl>
  </configuration>
</plugin>

In order to publish the contract after the test we need to include the pact:publish goal to the build command as shown below.

$ mvn clean package pact:publish

Now, we can switch the Pact Broker UI. As you see, there are several pacts generated during our tests. We can recognize it by the name of the consumer and provider.

quarkus-pact-broker

We can go to the details of each contract. Here’s the description of the integration between the department-service and employee-service.

Create Contract Test for Provider

Once we published pacts to the broker, we can proceed to the implementation of contract tests on the provider side. In the current case, it is employee-service. Firstly, let’s include the Quarkus Pact Provider extension in the Maven dependencies.

<dependency>
  <groupId>io.quarkiverse.pact</groupId>
  <artifactId>quarkus-pact-provider</artifactId>
  <version>1.0.0.Final</version>
  <scope>test</scope>
</dependency>

We need to annotate the test class with the @Provider annotation and pass the name of the provider used on the consumer side (1). In the @PactBroker annotation, we have to pass the address of the broker (2). The test will load the contract published by the consumer side and test it against the running instance of the Quarkus app (under the test instance port) (3). We also need to extend the test template with the PactVerificationInvocationContextProvider class (4). Thanks to that, Pact will trigger the verification of contracts for each interaction defined by the @State method (6) (7). We also let Pact publish the verification results of each contract to the Pact broker (5).

@QuarkusTest
@Provider("employee-service") // (1)
@PactBroker(url = "http://localhost:9292") // (2)
public class EmployeeContractTests {

   @ConfigProperty(name = "quarkus.http.test-port") 
   int quarkusPort;
    
   @TestTarget
   HttpTestTarget target = new HttpTestTarget("localhost", 
      this.quarkusPort); // (3)

   @TestTemplate
   @ExtendWith(PactVerificationInvocationContextProvider.class) // (4)
   void pactVerificationTestTemplate(PactVerificationContext context) {
      context.verifyInteraction();
      System.setProperty("pact.provider.version", "1.2"); 
      System.setProperty("pact.verifier.publishResults", "true"); // (5)
   }

   @BeforeEach
   void beforeEach(PactVerificationContext context) {
      context.setTarget(new HttpTestTarget("localhost",
         this.quarkusPort));
   }

   @State("findByDepartment") // (6)
   void findByDepartment() {

   }

   @State("findByOrganization") // (7)
   void findByOrganization() {

   }
}

The value of @State refers to the name of the integration set on the consumer side. For example, line (6) in the code source above verifies the contract defined in the department-sevice as shown below.

As I mentioned before, the contract verification results are published to the Pact broker. You can check the status of verification using Pact Broker UI:

quarkus-pact-verification

Run Tests in the CI Pipeline

Our last goal is to prepare the CI process for running Pact broker and contract tests during the build of our Quarkus apps. We will use CircleCI for that. Before running the contract tests we need to run the Pact broker container using Docker Compose. In order to do that we first need to use the Linux machine as a default executor and the docker orb (1). After that, we need to install Docker Compose and then use it to run the already prepared configuration in our docker-compose.yml file (2) (3). Then we can use the maven orb to run tests and publish contracts to the instance of the broker running during the tests (4).

version: 2.1

jobs:
  analyze:
    executor: # (1)
      name: docker/machine
      image: ubuntu-2204:2022.04.2
    steps:
      - checkout
      - docker/install-docker-compose # (2)
      - maven/with_cache:
          steps:
            - run:
                name: Build Images
                command: mvn package -DskipTests -Dquarkus.container-image.build=true
      - run: # (3)
          name: Run Pact Broker
          command: docker-compose up -d
      - maven/with_cache: # (4)
          steps:
            - run:
                name: Run Tests
                command: mvn package pact:publish -Dquarkus.container-image.build=false
      - maven/with_cache:
          steps:
            - run:
                name: Sonar Analysis
                command: mvn package sonar:sonar -DskipTests -Dquarkus.container-image.build=false


orbs:
  maven: circleci/maven@1.4.1
  docker: circleci/docker@2.2.0

workflows:
  maven_test:
    jobs:
      - analyze:
          context: SonarCloud

Here’s the final result of our build.

Final Thoughts

Contract testing is a useful approach for verifying interactions between microservices. Thanks to the Quarkus Pact extensions you can easily implement contract tests for your Quarkus apps. In this article, I showed how to use a Pact broker to store and share contracts between the tests. However, you can as well use the @PactFolder options to keep the contract JSON manifests inside the Git repository.

The post Contract Testing with Quarkus and Pact appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2023/05/09/contract-testing-with-quarkus-and-pact/feed/ 0 14166
Testing Microservices: Tools and Frameworks https://piotrminkowski.com/2018/09/06/testing-microservices-tools-and-frameworks/ https://piotrminkowski.com/2018/09/06/testing-microservices-tools-and-frameworks/#respond Thu, 06 Sep 2018 07:42:47 +0000 https://piotrminkowski.wordpress.com/?p=6797 There are some key challenges around a testing microservices architecture that we are facing. The selection of the right tools is one of those elements that help us deal with the issues related to those challenges. First, let’s identify the most important elements involved in the process of microservices testing. These are some of them: […]

The post Testing Microservices: Tools and Frameworks appeared first on Piotr's TechBlog.

]]>
There are some key challenges around a testing microservices architecture that we are facing. The selection of the right tools is one of those elements that help us deal with the issues related to those challenges. First, let’s identify the most important elements involved in the process of microservices testing. These are some of them:

  • Teams coordination – with many independent teams managing their own microservices, it becomes very challenging to coordinate the overall process of software development and testing
  • Complexity – there are many microservices that communicate with each other. We need to ensure that every one of them is working properly and is resistant to the slow responses or failures from other microservices
  • Performance – since there are many independent services it is essential to test the whole architecture under traffic close to the production

Let’s discuss some interesting frameworks helping that may help you test microservices-based architecture.

Components tests with Hoverfly

Hoverfly simulation mode may be beneficial for building component tests. During component tests, we are verifying the whole microservice without communication over the network with other microservices or external data stores. The following picture shows how such a test is performed for our sample microservice.

testing-microservices-1

Hoverfly provides simple DSL for creating simulations, and a JUnit integration for using it within JUnit tests. It may be orchestrated via JUnit @Rule. We are simulating two services and then overriding Ribbon properties to resolve an address of these services by client name. We should also disable communication with Eureka discovery by disabling registration after the application boot or fetching a list of services for a Ribbon client. Hoverfly simulates responses for PUT and GET methods exposed by passenger-management and driver-management microservices. A controller is the main component that implements business logic in our application. It stores data using an in-memory repository component and communicates with other microservices through @FeignClient interfaces. By testing three methods implemented by the controller we are testing the whole business logic implemented inside the trip-management service.

@SpringBootTest(properties = {
   "eureka.client.enabled=false",
   "ribbon.eureka.enable=false",
   "passenger-management.ribbon.listOfServers=passenger-management",
   "driver-management.ribbon.listOfServers=driver-management"
})
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TripComponentTests {

    ObjectMapper mapper = new ObjectMapper();

    @Autowired
    MockMvc mockMvc;
    @ClassRule
    public static HoverflyRule rule = HoverflyRule.inSimulationMode(SimulationSource.dsl(
            HoverflyDsl.service("passenger-management:80")
                    .get(HoverflyMatchers.startsWith("/passengers/login/"))

                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'John Walker'}")))
                    .put(HoverflyMatchers.startsWith("/passengers")).anyBody()
                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'John Walker'}"))),
            HoverflyDsl.service("driver-management:80")
                    .get(HoverflyMatchers.startsWith("/drivers/"))
                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'David Smith','currentLocationX': 15,'currentLocationY':25}")))
                    .put(HoverflyMatchers.startsWith("/drivers")).anyBody()
                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'David Smith','currentLocationX': 15,'currentLocationY':25}")))
    )).printSimulationData();

    @Test
    public void test1CreateNewTrip() throws Exception {
        TripInput ti = new TripInput("test", 10, 20, "walker");
        mockMvc.perform(MockMvcRequestBuilders.post("/trips")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(mapper.writeValueAsString(ti)))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.any(Integer.class)))
                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("NEW")))
                .andExpect(MockMvcResultMatchers.jsonPath("$.driverId", Matchers.any(Integer.class)));
    }

    @Test
    public void test2CancelTrip() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.put("/trips/cancel/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(mapper.writeValueAsString(new Trip())))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.any(Integer.class)))
                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("IN_PROGRESS")))
                .andExpect(MockMvcResultMatchers.jsonPath("$.driverId", Matchers.any(Integer.class)));
    }

    @Test
    public void test3PayTrip() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.put("/trips/payment/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(mapper.writeValueAsString(new Trip())))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.any(Integer.class)))
                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("PAYED")));
    }
}

The tests visible above verify only positive scenarios. What about testing some unexpected behavior like network delays or server errors? With Hoverfly we can easily simulate such behavior and define some negative scenarios. In the following fragment of code, I have defined three scenarios. In the first of them, the target service has been delayed 2 seconds. In order to simulate timeout on the client side I had to change the default readTimeout for the Ribbon load balancer and then disabled the Hystrix circuit breaker for Feign client. The second test simulates the HTTP 500 response status from the passenger-management service. The last scenario assumes an empty response from the method responsible for searching for the nearest driver.

@SpringBootTest(properties = {
        "eureka.client.enabled=false",
        "ribbon.eureka.enable=false",
        "passenger-management.ribbon.listOfServers=passenger-management",
        "driver-management.ribbon.listOfServers=driver-management",
        "feign.hystrix.enabled=false",
        "ribbon.ReadTimeout=500"
})
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
public class TripNegativeComponentTests {
    private ObjectMapper mapper = new ObjectMapper();
    @Autowired
    private MockMvc mockMvc;

    @ClassRule
    public static HoverflyRule rule = HoverflyRule.inSimulationMode(SimulationSource.dsl(
            HoverflyDsl.service("passenger-management:80")
                    .get("/passengers/login/test1")
                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'John Smith'}")).withDelay(2000, TimeUnit.MILLISECONDS))
                    .get("/passengers/login/test2")
                    .willReturn(ResponseCreators.success(HttpBodyConverter.jsonWithSingleQuotes("{'id':1,'name':'John Smith'}")))
                    .get("/passengers/login/test3")
                    .willReturn(ResponseCreators.serverError()),
            HoverflyDsl.service("driver-management:80")
                    .get(HoverflyMatchers.startsWith("/drivers/"))
                    .willReturn(ResponseCreators.success().body("{}"))
            ));

    @Test
    public void testCreateTripWithTimeout() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.post("/trips").contentType(MediaType.APPLICATION_JSON).content(mapper.writeValueAsString(new TripInput("test", 15, 25, "test1"))))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.nullValue()))
                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("REJECTED")));
    }

    @Test
    public void testCreateTripWithError() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.post("/trips").contentType(MediaType.APPLICATION_JSON).content(mapper.writeValueAsString(new TripInput("test", 15, 25, "test3"))))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.nullValue()))
                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("REJECTED")));
    }

    @Test
    public void testCreateTripWithNoDrivers() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.post("/trips").contentType(MediaType.APPLICATION_JSON).content(mapper.writeValueAsString(new TripInput("test", 15, 25, "test2"))))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.nullValue()))
                .andExpect(MockMvcResultMatchers.jsonPath("$.status", Matchers.is("REJECTED")));
    }
}

All the timeouts and errors in communication with external microservices are handled by the bean annotated with @ControllerAdvice. In such cases trip-management microservice should not return a server error response, but 200 OK with JSON response containing field status equals to REJECTED.


@ControllerAdvice
public class TripControllerErrorHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({RetryableException.class, FeignException.class})
    protected ResponseEntity handleFeignTimeout(RuntimeException ex, WebRequest request) {
        Trip trip = new Trip();
        trip.setStatus(TripStatus.REJECTED);
        return handleExceptionInternal(ex, trip, null, HttpStatus.OK, request);
    }

}

 

Contract testing with Pact framework

The next type of test strategy usually implemented for microservices-based architecture is consumer-driven contract testing. In fact, there are some tools especially dedicated to such a type of test. One of them is Pact. Contract testing is a way to ensure that services can communicate with each other without implementing integration tests. A contract is signed between two sides of communication: the consumer and the provider. Pact assumes that contract code is generated and published on the consumer side, and then verified by the provider.

Pact provides tools that can store and share the contracts between consumers and providers. It is called Pact Broker. It exposes a simple RESTful API for publishing and retrieving pacts, and embedded web dashboard for navigating the API. We can easily run Pact Broker on the local machine using its Docker image.

micro-testing-2

We will begin by running Pact Broker. Pact Broker requires running an instance of PostgreSQL, so first we have to launch it using a Docker image, and then link our broker container with that container.

$ docker run -d --name postgres postgres \
  -p 5432:5432 \
  -e POSTGRES_USER=oauth \ 
  -e POSTGRES_PASSWORD=oauth123 \
  -e POSTGRES_DB=oauth

$ docker run -d --name pact-broker dius/pact-broker \
  --link postgres:postgres \
  -e PACT_BROKER_DATABASE_USERNAME=oauth \
  -e PACT_BROKER_DATABASE_PASSWORD=oauth123 \ 
  -e PACT_BROKER_DATABASE_HOST=postgres \
  -e PACT_BROKER_DATABASE_NAME=oauth \
  -p 9080:80

The next step is to implement contract tests on the consumer side. We will use the JVM implementation of the Pact library for that. It provides a PactProviderRuleMk2 object responsible for creating stubs of the provider service. We should annotate it with JUnit @Rule. Ribbon will forward all requests to passenger-management to the stub address – in that case localhost:8180. Pact JVM supports annotations and provides DSL for building test scenarios. The test method responsible for generating contract data should be annotated with @Pact. It is important to set fields state and provider because then the generated contract would be verified on the provider side using these names. Generated pacts are verified inside the same test class by the methods annotated with @PactVerification. Field fragment points to the name of the method responsible for generating pact inside the same test class. The contract is tested using PassengerManagementClient @FeignClient.

@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
        "driver-management.ribbon.listOfServers=localhost:8190",
        "passenger-management.ribbon.listOfServers=localhost:8180",
        "ribbon.eureka.enabled=false",
        "eureka.client.enabled=false",
        "ribbon.ReadTimeout=5000"
})
public class PassengerManagementContractTests {
    @Rule
    public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("passengerManagementProvider", "localhost", 8180, this);
    @Autowired
    private PassengerManagementClient passengerManagementClient;

    @Pact(state = "get-passenger", provider = "passengerManagementProvider", consumer = "passengerManagementClient")
    public RequestResponsePact callGetPassenger(PactDslWithProvider builder) {
        DslPart body = new PactDslJsonBody().integerType("id").stringType("name").numberType("balance").close();
        return builder.given("get-passenger").uponReceiving("test-get-passenger")
                .path("/passengers/login/test").method("GET").willRespondWith().status(200).body(body).toPact();
    }

    @Pact(state = "update-passenger", provider = "passengerManagementProvider", consumer = "passengerManagementClient")
    public RequestResponsePact callUpdatePassenger(PactDslWithProvider builder) {
        return builder.given("update-passenger").uponReceiving("test-update-passenger")
                .path("/passengers").method("PUT").bodyWithSingleQuotes("{'id':1,'amount':1000}", "application/json").willRespondWith().status(200)
                .bodyWithSingleQuotes("{'id':1,'name':'Adam Smith','balance':5000}", "application/json").toPact();
    }

    @Test
    @PactVerification(fragment = "callGetPassenger")
    public void verifyGetPassengerPact() {
        Passenger passenger = passengerManagementClient.getPassenger("test");
        Assert.assertNotNull(passenger);
        Assert.assertNotNull(passenger.getId());
    }

    @Test
    @PactVerification(fragment = "callUpdatePassenger")
    public void verifyUpdatePassengerPact() {
        Passenger passenger = passengerManagementClient.updatePassenger(new PassengerInput(1L, 1000));
        Assert.assertNotNull(passenger);
        Assert.assertNotNull(passenger.getId());
    }
}

Just running the tests is not enough. We also have to publish pacts generated during tests to Pact Broker. In order to achieve it we have to include the following Maven plugin to our pom.xml and then execute command mvn clean install pact:publish.

<plugin>
   <groupId>au.com.dius</groupId>
   <artifactId>pact-jvm-provider-maven_2.12</artifactId>
   <version>3.5.21</version>
   <configuration>
      <pactBrokerUrl>http://192.168.99.100:9080</pactBrokerUrl>
   </configuration>
</plugin>

Pact provides support for Spring on the provider side. Thanks to that we may use MockMvc controllers or inject properties from application.yml into the test class. Here’s the dependency declaration that has to be included to our pom.xml

<dependency>
   <groupId>au.com.dius</groupId>
   <artifactId>pact-jvm-provider-spring_2.12</artifactId>
   <version>3.5.21</version>
   <scope>test</scope>
</dependency>

Now, the contract is being verified on the provider side. We need to pass the provider name inside @Provider annotation and name of states for every verification test inside @State. These values have been during the tests on the consumer side inside @Pact annotation (fields state and provider).

@RunWith(SpringRestPactRunner.class)
@Provider("passengerManagementProvider")
@PactBroker
public class PassengerControllerContractTests {
    @InjectMocks
    private PassengerController controller = new PassengerController();
    @Mock
    private PassengerRepository repository;
    @TestTarget
    public final MockMvcTarget target = new MockMvcTarget();

    @Before
    public void before() {
        MockitoAnnotations.initMocks(this);
        target.setControllers(controller);
    }

    @State("get-passenger")
    public void testGetPassenger() {
        target.setRunTimes(3);
        Mockito.when(repository.findByLogin(Mockito.anyString()))
                .thenReturn(new Passenger(1L, "Adam Smith", "test", 4000))
                .thenReturn(new Passenger(3L, "Tom Hamilton", "hamilton", 400000))
                .thenReturn(new Passenger(5L, "John Scott", "scott", 222));
    }

    @State("update-passenger")
    public void testUpdatePassenger() {
        target.setRunTimes(1);
        Passenger passenger = new Passenger(1L, "Adam Smith", "test", 4000);
        Mockito.when(repository.findById(1L)).thenReturn(passenger);
        Mockito.when(repository.update(Mockito.any(Passenger.class)))
                .thenReturn(new Passenger(1L, "Adam Smith", "test", 5000));
    }
}

The Pact Broker host and port are injected from application.yml file.

pactbroker:
  host: "192.168.99.100"
  port: "8090"

 

Performance tests with Gatling

An important step of testing microservices before deploying them on production is performance testing. One of the interesting tools in this area is Gatling. It is a highly capable load-testing tool written in Scala. It means that we also have to use Scala DSL in order to build test scenarios. Let’s begin by adding the required library to pom.xml file.

<dependency>
   <groupId>io.gatling.highcharts</groupId>
   <artifactId>gatling-charts-highcharts</artifactId>
   <version>2.3.1</version>
</dependency>

Now, we may proceed to the test. In the scenario visible above we are testing two endpoints exposed by trip-management: POST /trips and PUT /trips/payment/${tripId}. In fact, this scenario verifies the whole functionality of our sample system, where we are setting up a trip and then paying for it after a finish.
Every test class using Gatling needs to extend the Simulation class. We are defining the scenario using the scenario method and then set its name. We may define multiple executions inside a single scenario. After every execution of the POST /trips method, the test save generated id returned by the service. Then it inserts that id into the URL used for calling method PUT /trips/payment/${tripId}. Every single test expects a response with 200 OK status.

Gatling provides two interesting features, which are worth mentioning. You can see how they are used in the following performance test. First of all, it is a feeder. It is used for polling records and injecting their content into the test. Feed rPassengers selects one of five defined logins randomly. The final test result may be verified using Assertions API. It is responsible for verifying global statistics like response time or number of failed requests matches expectations for a whole simulation. In the scenario visible below the criterium is max response time that needs to be lower 100 milliseconds.

class CreateAndPayTripPerformanceTest extends Simulation {

  val rPassengers = Iterator.continually(Map("passenger" -> List("walker","smith","hamilton","scott","holmes").lift(Random.nextInt(5)).get))

  val scn = scenario("CreateAndPayTrip").feed(rPassengers).repeat(100, "n") {
    exec(http("CreateTrip-API")
      .post("http://localhost:8090/trips")
      .header("Content-Type", "application/json")
      .body(StringBody("""{"destination":"test${n}","locationX":${n},"locationY":${n},"username":"${passenger}"}"""))
      .check(status.is(200), jsonPath("$.id").saveAs("tripId"))
    ).exec(http("PayTrip-API")
      .put("http://localhost:8090/trips/payment/${tripId}")
      .header("Content-Type", "application/json")
      .check(status.is(200))
    )
  }

  setUp(scn.inject(atOnceUsers(20))).maxDuration(FiniteDuration.apply(5, TimeUnit.MINUTES))
    .assertions(global.responseTime.max.lt(100))
}

In order to run a Gatling performance test you need to include the following Maven plugin to your pom.xml. You may run a single scenario or run multiple scenarios. After including the plugin you only need to execute the command mvn clean gatling:test.

<plugin>
   <groupId>io.gatling</groupId>
   <artifactId>gatling-maven-plugin</artifactId>
   <version>2.2.4</version>
   <configuration>
      <simulationClass>pl.piomin.performance.tests.CreateAndPayTripPerformanceTest</simulationClass>
   </configuration>
</plugin>

Here are some diagrams illustrating the results of performance tests for our microservice. Because the maximum response time has been greater than set inside assertion (100ms), the test has failed.

microservices-testing-2

and …

microservices-testing-3

 

Summary

The right selection of tools is not the most important element phase of microservices testing. However, the right tools can help you face the key challenges related to it. Hoverfly allows you to create full component tests that verifies if your microservice is able to handle delays or errors from downstream services. Pact helps you to organize a team by sharing and verifying contracts between independently developed microservices. Finally, Gatling can help you implement load tests for selected scenarios, in order to verify the end-to-end performance of your system.
The source code used as a demo for this article is available on GitHub: https://github.com/piomin/sample-testing-microservices.git. If you find this article interesting for you you may be also interested in some other articles related to this subject:

The post Testing Microservices: Tools and Frameworks appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2018/09/06/testing-microservices-tools-and-frameworks/feed/ 0 6797