testing Archives - Piotr's TechBlog https://piotrminkowski.com/tag/testing/ Java, Spring, Kotlin, microservices, Kubernetes, containers Mon, 27 Nov 2023 09:32:27 +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 testing Archives - Piotr's TechBlog https://piotrminkowski.com/tag/testing/ 32 32 181738725 Testing Java Apps on Kubernetes with Testkube https://piotrminkowski.com/2023/11/27/testing-java-apps-on-kubernetes-with-testkube/ https://piotrminkowski.com/2023/11/27/testing-java-apps-on-kubernetes-with-testkube/#respond Mon, 27 Nov 2023 09:32:12 +0000 https://piotrminkowski.com/?p=14684 In this article, you will learn how to test Java apps on Kubernetes with Testkube automatically. We will build the tests for the typical Spring REST-based app. In the first scenario, Testkube runs the JUnit tests using its Maven support. After that, we will run the load tests against the running instance of our app […]

The post Testing Java Apps on Kubernetes with Testkube appeared first on Piotr's TechBlog.

]]>
In this article, you will learn how to test Java apps on Kubernetes with Testkube automatically. We will build the tests for the typical Spring REST-based app. In the first scenario, Testkube runs the JUnit tests using its Maven support. After that, we will run the load tests against the running instance of our app using the Grafana k6 tool. Once again, Kubetest provides a standard mechanism for that, no matter which tool we use for testing.

If you are interested in testing on Kubernetes you can also read my article about integration tests with JUnit. There is also a post about contract testing on Kubernetes with Microcks available here.

Introduction

Testkube is a Kubernetes native test orchestration and execution framework. It allows us to run automated tests inside the Kubernetes cluster. It supports several popular testing or build tools like JMeter, Grafana k6, and Maven. We can easily integrate with the CI/CD pipelines or GitOps workflows. We can manage Kubetest by using the CRD objects directly, with the CLI, or through the UI dashboard. Let’s check how it works.

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. It contains only a single app. Once you clone it you can go to the src/test directory. You will find there both the JUnit tests written in Java and the k6 tests written in JavaScript. After that, you should just follow my instructions. Let’s begin.

Run Kubetest on Kubernetes

In the first step, we are going to install Testkube on Kubernetes using its Helm chart. Let’s add the kubeshop Helm repository and fetch latest charts info:

$ helm repo add kubeshop https://kubeshop.github.io/helm-charts
$ helm repo update

Then, we can install Testkube in the testkube namespace by executing the following helm command:

$ helm install testkube kubeshop/testkube \
    --create-namespace --namespace testkube

This will add custom resource definitions (CRD), RBAC roles, and role bindings to the Kubernetes cluster. This installation requires having cluster administrative rights.

Once the installation is finished, we can verify a list of running in the testkube namespace. The testkube-api-server and testkube-dashboard are the most important components. However, there are also some additional tools installed like Mongo database or Minio.

$ oc get po -n testkube
NAME                                                    READY   STATUS    RESTARTS        AGE
testkube-api-server-d4d7f9f8b-xpxc9                     1/1     Running   1 (6h17m ago)   6h18m
testkube-dashboard-64578877c7-xghsz                     1/1     Running   0               6h18m
testkube-minio-testkube-586877d8dd-8pmmj                1/1     Running   0               6h18m
testkube-mongodb-dfd8c7878-wzkbp                        1/1     Running   0               6h18m
testkube-nats-0                                         3/3     Running   0               6h18m
testkube-nats-box-567d94459d-6gc4d                      1/1     Running   0               6h18m
testkube-operator-controller-manager-679b998f58-2sv2x   2/2     Running   0               6h18m

We can also install testkube CLI on our laptop. It is not required, but we will use it during the exercise just try the full spectrum of options. You can find CLI installation instructions here. I’m installing it on macOS:

$ brew install testkube

Once the installation is finished, you can run the testkube version command to see that warm “Hello” screen 🙂

testkube-kubernetes-cli

Run Maven Tests with Testkube

Firstly, let’s take a look at the JUnit tests inside our sample Spring Boot app. We are using the TestRestTemplate bean to call all the exposed REST endpoints exposed. There are three JUnit tests for testing adding, getting, and removing the Person objects.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class PersonControllerTests {

   @Autowired
   lateinit var template: TestRestTemplate

   @Test
   @Order(1)
   fun shouldAddPerson() {
      var person = Instancio.of(Person::class.java)
         .ignore(Select.field("id"))
         .create()
      person = template
         .postForObject("/persons", person, Person::class.java)
      Assertions.assertNotNull(person)
      Assertions.assertNotNull(person.id)
      Assertions.assertEquals(1001, person.id)
   }

   @Test
   @Order(2)
   fun shouldUpdatePerson() {
      var person = Instancio.of(Person::class.java)
         .set(Select.field("id"), 1)
         .create()
      template.put("/persons", person)
      var personRemote = template
         .getForObject("/persons/{id}", Person::class.java, 1)
      Assertions.assertNotNull(personRemote)
      Assertions.assertEquals(person.age, personRemote.age)
   }

   @Test
   @Order(3)
   fun shouldDeletePerson() {
      template.delete("/persons/{id}", 1)
      val person = template
         .getForObject("/persons/{id}", Person::class.java, 1)
      Assertions.assertNull(person)
   }

}

We are using Maven as a build tool. The current version of Spring Boot is 3.2.0. The version of JDK used for the compilation is 17. Here’s the fragment of our pom.xml in the repository root directory:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
  </parent>
  <groupId>pl.piomin.services</groupId>
  <artifactId>sample-spring-kotlin-microservice</artifactId>
  <version>1.5.3</version>

  <properties>
    <java.version>17</java.version>
    <kotlin.version>1.9.21</kotlin.version>
  </properties>

  <dependencies>
    ...   
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.instancio</groupId>
      <artifactId>instancio-junit</artifactId>
      <version>3.6.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Testkube provides the Executor CRD for defining a way of running each test. There are several default executors per each type of supported build or test tool. We can display a list of provided executors by running the testkube get executor command. You will see the list of all tools supported by Testkube. Of course, the most interesting executors for us are k6-executor and maven-executor.

$ testkube get executor

Context:  (1.16.8)   Namespace: testkube
----------------------------------------

  NAME                 | URI | LABELS
-----------------------+-----+-----------------------------------
  artillery-executor   |     |
  curl-executor        |     |
  cypress-executor     |     |
  ginkgo-executor      |     |
  gradle-executor      |     |
  jmeter-executor      |     |
  jmeterd-executor     |     |
  k6-executor          |     |
  kubepug-executor     |     |
  maven-executor       |     |
  playwright-executor  |     |
  postman-executor     |     |
  soapui-executor      |     |
  tracetest-executor   |     |
  zap-executor         |     |

By default, maven-executor uses JDK 11 for running Maven tests. Moreover, it still doesn’t provide images for running tests against JDK19+. For me, this is quite a big drawback since the latest LTS version of Java is 21. The maven-executor-jdk17 Executor contains the name of the running image (1) and a list of supported test types (2).

apiVersion: executor.testkube.io/v1
kind: Executor
metadata:
  name: maven-executor-jdk17
  namespace: testkube
spec:
  args:
    - '--settings'
    - <settingsFile>
    - <goalName>
    - '-Duser.home'
    - <mavenHome>
  command:
    - mvn
  content_types:
    - git-dir
    - git
  executor_type: job
  features:
    - artifacts
  # (1)
  image: kubeshop/testkube-maven-executor:jdk17 
  meta:
    docsURI: https://kubeshop.github.io/testkube/test-types/executor-maven
    iconURI: maven
  # (2)
  types:
    - maven:jdk17/project
    - maven:jdk17/test
    - maven:jdk17/integration-test

Finally, we just need to define the Test object that references to maven-executor-jdk17 by the type parameter. Of course, we also need to set the address of the Git repository and the name of the branch.

apiVersion: tests.testkube.io/v3
kind: Test
metadata:
  name: sample-spring-kotlin
  namespace: testkube
spec:
  content:
    repository:
      branch: master
      type: git
      uri: https://github.com/piomin/sample-spring-kotlin-microservice.git
    type: git
  type: maven:jdk17/test

Finally, we can run the sample-spring-kotlin test using the following command:

$ testkube run test sample-spring-kotlin

Using UI Dashboard

First of all, let’s expose the Testkube UI dashboard on the local port. The dashboard also requires a connection to the testkube-api-server from the web browser. After exposing the dashboard with the following port-forward command we can access it under the http://localhost:8080 address:

$ kubectl port-forward svc/testkube-dashboard 8080 -n testkube
$ kubectl port-forward svc/testkube-api-server 8088 -n testkube

Once we access the Testkube dashboard we will see a list of all defined tests:

testkube-kubernetes-ui

Then, we can click the selected tile with the test to see the details. You will be redirected to the history of previous executions available in the “Recent executions” tab. There are six previous executions of our sample-spring-kotlin test. Two of them were finished successfully, the four others were failed.

Let’s take a look at the logs of the last one execution. As you see, all three JUnit tests were successful.

testkube-kubernetes-test-logs

Run Load Tests with Testkube and Grafana k6

In this section, we will create the tests for the instance of our sample app running on Kubernetes. So, in the first step, we need to deploy the app. Here’s the Deployment manifest. We can apply it to the default namespace. The manifest uses the latest image of the sample app available in the registry under the quay.io/pminkows/sample-kotlin-string:1.5.3 address.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-kotlin-spring
  labels:
    app: sample-kotlin-spring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-kotlin-spring
  template:
    metadata:
      labels:
        app: sample-kotlin-spring
    spec:
      containers:
      - name: sample-kotlin-spring
        image: quay.io/pminkows/sample-kotlin-spring:1.5.3
        ports:
        - containerPort: 8080

Let’s also create the Kubernetes Service that exposes app pods internally:

apiVersion: v1
kind: Service
metadata:
  name: sample-kotlin-spring
spec:
  selector:
    app: sample-kotlin-spring
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080

After that, we can proceed to the Test manifest. This time, we don’t have to override the default executor, since the k6 version is not important. The test source is located inside the sample Git repository in the src/test/resources/k6/load-tests-get.js (1) file in the master branch. In that case, the repository type is git (2). The k6 test should run for 5 seconds and should use 5 concurrent threads (3). We also need to set the address of a target service as the PERSONS_URI environment variable (4). Of course, we are testing through the Kubernetes Service visible internally under the sample-kotlin-spring.default.svc host and port 8080. The type of the test is k6/script (5).

apiVersion: tests.testkube.io/v3
kind: Test
metadata:
  labels:
    executor: k6-executor
    test-type: k6-script
  name: load-tests-gets
  namespace: testkube
spec:
  content:
    repository:
      branch: master
      # (1)
      path: src/test/resources/k6/load-tests-get.js
      # (2) 
      type: git
      uri: https://github.com/piomin/sample-spring-kotlin-microservice.git
    type: git
  executionRequest:
    # (3)
    args:
      - '-u'
      - '5'
      - '-d'
      - 10s
    # (4)
    variables:
      PERSONS_URI:
        name: PERSONS_URI
        type: basic
        value: http://sample-kotlin-spring.default.svc:8080
        valueFrom: {}
  # (5)
  type: k6/script

Let’s take a look at the k6 test file written in JavaScript. As I mentioned before, you can find it in the src/test/resources/k6/load-tests-get.js file. The test calls the GET /persons/{id} endpoint. It sets the random number between 1 and 1000 as the id path parameter and reads a target service URL from the PERSONS_URI environment variable.

import http from 'k6/http';
import { check } from 'k6';
import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';

export default function () {
  const id = randomIntBetween(1, 1000);
  const res = http.get(`${__ENV.PERSONS_URI}/persons/${id}`);
  check(res, {
    'is status 200': (res) => res.status === 200,
    'body size is > 0': (r) => r.body.length > 0,
  });
}

Finally, we can run the load-tests-gets test with the following command:

$ testkube run test load-tests-gets

The same as for the Maven test we can verify the execution history in the Testkube dashboard:

We can also display all the logs from the test:

Final Thoughts

Testkube provides a unified way to run Kubernetes tests for the several most popular testing tools. It may be a part of your CI/CD pipeline or a GitOps process. Honestly, I’m still not very convinced if I need a dedicated Kubernetes-native solution for automated tests, instead e.g. a stage in my pipeline that runs test commands. However, you can also use Testkube to execute load or integration tests against the app running on Kubernetes. It is possible to schedule them periodically. Thanks to that you can verify your apps continuously using a single, central tool.

The post Testing Java Apps on Kubernetes with Testkube appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2023/11/27/testing-java-apps-on-kubernetes-with-testkube/feed/ 0 14684
Create and Release Your Own Helm Chart https://piotrminkowski.com/2023/02/28/create-and-release-your-own-helm-chart/ https://piotrminkowski.com/2023/02/28/create-and-release-your-own-helm-chart/#respond Tue, 28 Feb 2023 14:32:18 +0000 https://piotrminkowski.com/?p=14038 In this article, you will learn how to create your Helm chart and release it in the public repository. We will prepare a Helm chart for the typical Spring Boot REST-based app as an exercise. Our goal is to have a fully automated process to build, test, and release it. In order to do that, […]

The post Create and Release Your Own Helm Chart appeared first on Piotr's TechBlog.

]]>
In this article, you will learn how to create your Helm chart and release it in the public repository. We will prepare a Helm chart for the typical Spring Boot REST-based app as an exercise. Our goal is to have a fully automated process to build, test, and release it. In order to do that, we will define a pipeline in CircleCI. This CI/CD pipeline will publish the Helm chart in the public Artifact Hub.

If you are interested in the Helm chart and CI/CD process you may refer to the following article. It shows how to design your continuous development process on Kubernetes and use Helm charts in the GitOps approach.

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.

Create Helm Chart

In this part of the exercise, we will use the helm CLI. Helm installed locally is not required in our whole process, but helps you understand what will happen in the next steps. Therefore, it is worth installing it. Please refer to the official Helm docs to find an installation approach suitable for your needs.

In the first step, we are going to create a sample chart. It is a typical chart for web apps. For example, it exposes the 8080 port outside of the containers or allows us to define liveness and readiness probes checking HTTP endpoints. This Helm chart should not be too complicated, but also not too simple, since we want to create automated tests for it.

Here’s our Deployment template. It adds some standard labels to the Deployment manifest (1). It also sets resource requests and limits (2). As I mentioned before, our chart is adding liveness probe (3), readiness probe (4), and exposes port 8080 outside of the container (5). We may also set environment variables (6), or inject them from ConfigMap (7) and Secret (8).

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.app.name }}
  labels: # (1)
    app: {{ .Values.app.name }}
    env: {{ .Values.app.environment }}
    owner: {{ .Values.app.owner }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Values.app.name }}
  template:
    metadata:
      labels:
        app: {{ .Values.app.name }}
        env: {{ .Values.app.environment }}
    spec:
      containers:
        - name: {{ .Values.app.name }}
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          resources: # (2)
            {{- toYaml .Values.resources | nindent 12 }}
          livenessProbe: # (3)
            initialDelaySeconds: {{ .Values.liveness.initialDelaySeconds }}
            httpGet:
              port: {{ .Values.liveness.port }}
              path: {{ .Values.liveness.path }}
            failureThreshold: {{ .Values.liveness.failureThreshold }}
            successThreshold: {{ .Values.liveness.successThreshold }}
            timeoutSeconds: {{ .Values.liveness.timeoutSeconds }}
            periodSeconds: {{ .Values.liveness.periodSeconds }}
          readinessProbe: # (4)
            initialDelaySeconds: {{ .Values.readiness.initialDelaySeconds }}
            httpGet:
              port: {{ .Values.readiness.port }}
              path: {{ .Values.readiness.path }}
            failureThreshold: {{ .Values.readiness.failureThreshold }}
            successThreshold: {{ .Values.readiness.successThreshold }}
            timeoutSeconds: {{ .Values.readiness.timeoutSeconds }}
            periodSeconds: {{ .Values.readiness.periodSeconds }}
          ports: # (5)
          {{- range .Values.ports }}
          - containerPort: {{ .value }}
            name: {{ .name }}
          {{- end }}
          {{- if .Values.envs }}
          env: # (6)
          {{- range .Values.envs }}
          - name: {{ .name }}
            value: {{ .value }}
          {{- end }}
          {{- end }}
          {{- if or .Values.extraEnvVarsConfigMap .Values.extraEnvVarsSecret }}
          envFrom:
          {{- if or .Values.extraEnvVarsConfigMap }}
          - configMapRef:
              name: {{ .Values.extraEnvVarsConfigMap }}
          {{- end }}
          {{- if or .Values.extraEnvVarsSecret }}
          - secretRef:
              name: {{ .Values.extraEnvVarsSecret }}
          {{- end }}
          {{- end }}
          securityContext:
            runAsNonRoot: true

We also have a template for the Service object.

apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.app.name }}
  labels:
    app: {{ .Values.app.name }}
    env: {{ .Values.app.environment }}
    owner: {{ .Values.app.owner }}
spec:
  type: {{ .Values.service.type }}
  selector:
    app: {{ .Values.app.name }}
  ports:
  {{- range .Values.ports }}
  - port: {{ .value }}
    name: {{ .name }}
  {{- end }}

Now, we can fill our templates with the default values. The following values.yaml file is available in the sample repository.

replicaCount: 1

app:
  name: sample-spring-boot-api
  environment: dev
  owner: default

image:
  repository: piomin/sample-spring-kotlin-microservice
  tag: "1.1"

nameOverride: ""
fullnameOverride: ""

service:
  type: ClusterIP

ports:
  - name: http
    value: 8080

resources:
  limits:
    cpu: 1000m
    memory: 512Mi
  requests:
    cpu: 100m
    memory: 256Mi

liveness:
  initialDelaySeconds: 10
  port: http
  path: /actuator/health/liveness
  failureThreshold: 3
  successThreshold: 1
  timeoutSeconds: 3
  periodSeconds: 5

readiness:
  initialDelaySeconds: 10
  port: http
  path: /actuator/health/readiness
  failureThreshold: 3
  successThreshold: 1
  timeoutSeconds: 3
  periodSeconds: 5

envs:
  - name: INFO
    value: Spring Boot REST API

You can easily test the newly created templates with the helm CLI. In order to do that, just execute the following command in the repository root directory. As a result, you will see the YAML manifests created from our sample templates.

$ helm template charts/spring-boot-api-app

Such a testing method is fine… but just to run it locally during chart development. Assuming we need to create a delivery pipeline, we need a more advanced tool.

Unit Testing of Helm Charts

From my perspective, the most important thing in the CI/CD pipeline is automated testing. Without it, we are releasing unverified software, which may potentially result in many complications. The single Helm chart can be by several apps, so we should make every effort to carefully test it. Fortunately, there are some tools dedicated to Helm chart testing. My choice fell on the helm-unittest. It allows us to write unit tests file in pure YAML. We can install it as a Helm plugin or run it inside the Docker container. Let’s just it locally to verify our test work before pushing it to the Git repository:

$ helm plugin install https://github.com/helm-unittest/helm-unittest

We should place the unit test inside the test directory in our chart. Here’s the structure of our chart repository:

helm-chart-release-files

In the first step, we are creating the unit test file. As mentioned before, we can create a test using the YAML notation. It is pretty intuitive. We need to pass a location of the values file (1) and a location of the tested Helm template (2). In the test section, we have to define a list of asserts (3). I will not get into the details of the helm-unittest tool – for more information please refer to its docs. The important thing is that I can easily test each path of the YAML manifest. It can be an exact comparison or regex. It also supports JsonPath for mappings and arrays. Here’s our test in the deployment_test.yaml:

suite: test deployment
values:
  - ./values/test.yaml # (1)
templates:
  - templates/deployment.yaml # (2)
chart:
  version: 0.3.4+test
  appVersion: 1.0.0
tests:
  - it: should pass all kinds of assertion
    template: templates/deployment.yaml
    documentIndex: 0
    asserts: # (3)
      - equal:
          path: spec.template.spec.containers[?(@.name == "sample-spring-boot-api")].image
          value: piomin/sample-spring-kotlin-microservice:1.1
      - equal:
          path: metadata.labels.app
          value: sample-spring-boot-api
      - equal:
          path: metadata.labels.env
          value: dev
      - equal:
          path: metadata.labels.owner
          value: default
      - matchRegex:
          path: metadata.name
          pattern: ^.*-api$
      - contains:
          path: spec.template.spec.containers[?(@.name == "sample-spring-boot-api")].ports
          content:
            containerPort: 8080
            name: http
      - notContains:
          path: spec.template.spec.containers[?(@.name == "sample-spring-boot-api")].ports
          content:
            containerPort: 80
      - isNotEmpty:
          path: spec.template.spec.containers[?(@.name == "sample-spring-boot-api")].livenessProbe
      - isNotEmpty:
          path: spec.template.spec.containers[?(@.name == "sample-spring-boot-api")].readinessProbe
      - isKind:
          of: Deployment
      - isAPIVersion:
          of: apps/v1

Now, we can verify the test locally by executing the following command from the project root directory:

$ helm unittest charts/*

Currently, we have only a single chart in the charts directory. Assuming we would have more, it runs the tests for all charts. Here’s our result:

helm-chart-release-test

We can change something in the test to break it. Now, the result of the same will look like that:

Helm Chart Release Pipeline in CircleCI

Once we have a chart and tests created we may proceed to the delivery pipeline. In our CircleCI pipeline, we have to do not only the same steps as before, but also need to include a release part. First of all, we will use GitHub Releases and GitHub Pages to release and host our charts. In order to simplify the process, we may use a dedicated tool for releasing Helm charts – Chart Releaser.

We also need to create a personal token to pass to the Helm Chart Release workflow. Visit Settings > Developer Settings > Personal Access Token. Generate Personal the token with the repo scope. Then, we should put this token into the CircleCI context. You can choose any name for the context, but the name of the environment variable has to be CR_TOKEN. That name is required by the Chart Releaser. The name of my context is GitHub.

Here’s the list of steps we need to do in our pipeline:

  1. Install the helm CLI on the machine (we will use the cimg/base image as the tests executor)
  2. Install the Helm unit-test plugin
  3. Run unit tests
  4. Only if we make a change in the master branch, then we proceed to the release part. In the first step, we need to package the chart with helm package command
  5. Install Chart Releaser
  6. Release the chart in GitHub with the Chart Releaser upload command
  7. Generate chart index.yaml and publish it on GitHub Pages

Let’s define our CircleCI pipeline. First, we need to create the .circleci directory in the repository root and place the config.yml file there. We can use the helm orb to simplify the process of the helm CLI installation (1) (2). Once we install the helm CLI, we can install the unit-test plugin and run the unit tests (3). Then we define a rule for filtering the master branch (4). If the change is pushed to the master branch we package the chart as the TAR archive and place it in the .deploy directory (5). Then we install Chart Releaser and create a GitHub release (6). In the last step, we generate the index.yaml file using Chart Releaser and commit it to the gh-pages branch (7).

version: 2.1

orbs:
  helm: circleci/helm@2.0.1 # (1)

jobs:
  build:
    docker:
      - image: cimg/base:2023.02
    steps:
      - checkout
      - helm/install-helm-client # (2)
      - run:
          name: Install Helm unit-test
          command: helm plugin install https://github.com/helm-unittest/helm-unittest
      - run: # (3)
          name: Run unit tests
          command: helm unittest charts/*
      - when:
          condition: # (4)
            equal: [ master, << pipeline.git.branch >> ]
          steps:
            - run:
                name: Package chart # (5)
                command: helm package charts/* -d .deploy
            - run:
                name: Install chart releaser
                command: |
                  curl -L -o /tmp/cr.tgz https://github.com/helm/chart-releaser/releases/download/v1.5.0/chart-releaser_1.5.0_linux_amd64.tar.gz
                  tar -xv -C /tmp -f /tmp/cr.tgz
                  mv /tmp/cr ~/bin/cr
            - run:
                name: Release chart # (6)
                command: cr upload -o piomin -r helm-charts -p .deploy
            - run:
                name: Create index on GitHub pages # (7)
                command: |
                  git config user.email "job@circleci.com"
                  git config user.name "CircleCI"
                  git checkout --orphan gh-pages
                  cr index -i ./index.yaml -p .deploy -o piomin -r helm-charts
                  git add index.yaml
                  git commit -m "New release"
                  git push --set-upstream --force origin gh-pages

workflows:
  helm_test:
    jobs:
      - build:
          context: GitHub

Execute Helm Chart Release Pipeline

Once we push a change to the helm-charts repository our pipeline is starting. Here is our result. As you see the pipeline finishes with success. We were releasing the 0.3.5 version of our chart.

Let’s see a list of GitHub releases. As you see, the 0.3.5 version has already been released.

How to access our Helm repository. In order to check it go to the repository Settings > Pages. The address of the GitHub Pages for that repository is the address of our Helm repository. We publish there the index.yaml file that contains a definition of charts inside the repository. As you see, the address of the Helm repository is piomin.github.io/helm-charts.

We can see the structure of the index.yaml file just by calling the following URL: https://piomin.github.io/helm-charts/index.yaml. Here’s the fragment of index.yaml for the currently published version.

apiVersion: v1
entries:
  spring-boot-api-app:
  - apiVersion: v2
    appVersion: 1.0.0
    created: "2023-02-28T13:06:28.835693321Z"
    description: A Helm chart for Kubernetes
    digest: b308cbdf9f93f79baf5b39de8a7c509834d7b858f33d79d0c76b528e0cd7ca11
    name: spring-boot-api-app
    type: application
    urls:
    - https://github.com/piomin/helm-charts/releases/download/spring-boot-api-app-0.3.5/spring-boot-api-app-0.3.5.tgz
    version: 0.3.5

Assuming we want to use our Helm chart we can easily access it. First, let’s add the Helm repository using CLI:

$ helm repo add piomin https://piomin.github.io/helm-charts/

Then, we can verify a list of Helm charts existing inside the repository:

$ helm search repo piomin
NAME                            CHART VERSION   APP VERSION     DESCRIPTION                
piomin/spring-boot-api-app      0.3.5           1.0.0           A Helm chart for Kubernetes

Publish Helm Chart to Artifact Hub

In order to publish your Helm repository and charts on Artifact Hub you need to go to that site and create an account. Once you do it, you can add a new repository just by clicking the button. Then you just need to choose the name of your repo and put the right address.

Now, we can find our spring-boot-api-app chart on the list of packages.

We can see its details. It’s worth publishing documentation in the README.md file. Once you do it, you can view it in the chart details on Artifact Hub.

helm-chart-release-artifacthub

Finally, we can easily use the chart and deploy the Spring Boot app, e.g. with Argo CD.

The post Create and Release Your Own Helm Chart appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2023/02/28/create-and-release-your-own-helm-chart/feed/ 0 14038
Useful & Unknown Java Libraries https://piotrminkowski.com/2023/01/30/useful-unknown-java-libraries/ https://piotrminkowski.com/2023/01/30/useful-unknown-java-libraries/#comments Mon, 30 Jan 2023 09:39:23 +0000 https://piotrminkowski.com/?p=13954 This article will teach you about some not famous but useful Java libraries. This is the second article in the “useful & unknown” series. The previous one described several attractive, but not well-known Java features. You can read more about it here. Today we will focus on Java libraries. Usually, we use several external libraries […]

The post Useful & Unknown Java Libraries appeared first on Piotr's TechBlog.

]]>
This article will teach you about some not famous but useful Java libraries. This is the second article in the “useful & unknown” series. The previous one described several attractive, but not well-known Java features. You can read more about it here.

Today we will focus on Java libraries. Usually, we use several external libraries in our projects – even if we do not include them directly. For example, Spring Boot comes with a defined set of dependencies included by starters. Assuming we include e.g. spring-boot-starter-test we include libraries like mockito, junit-jupiter or hamcrest at the same time. Of course, these are well-known libraries for the community.

In fact, there are a lot of different Java libraries. Usually, I don’t need to use many of them (or even I need none of them) when working with the frameworks like Spring Boot or Quarkus. However, there are some very interesting libraries that may be useful everywhere. I’m writing about them because you might not hear about any of them. I’m going to introduce 5 of my favorite “useful & unknown” Java libraries. Let’s begin!

Source Code

If you would like to try it by yourself, you may always take a look at my source code. To do that you need to clone my GitHub repository. Yo can also find the example Then you should just follow my instructions.

Instancio

On the first fire will go Instancio. How do you generate test data in your unit tests? Instancio will help us with that. It aims to reduce the time and lines of code spent on manual data setup in unit tests. It instantiates and populates objects with random data, making our tests more dynamic. We can generate random data with Instancio but at the same, we can set custom data in a particular field.

Before we start with Instancio, let’s discuss our data model. Here’s the first class – Person:

public class Person {

   private Long id;
   private String name;
   private int age;
   private Gender gender;
   private Address address;

   // getters and setters ...

}

Our class contains three simple fields (id, name, age), a single enum Gender , and the instance of the Address class. Gender is just a simple enum containing MALE and FEMALE values. Here’s the implementation of the Address class:

public class Address {

   private String country;
   private String city;
   private String street;
   private int houseNumber;
   private int flatNumber;

   // getters and setters ...

}

Now, let’s create a test to check whether the Person service will successfully add and obtain objects from the store. We want to generate random data for all the fields except the id field, which is set by the service. Here’s our test:

@Test
void addAndGet() {
   Person person = Instancio.of(Person.class)
             .ignore(Select.field(Person::getId))
             .create();
   person = personService.addPerson(person);
   Assertions.assertNotNull(person.getId());
   person = personService.findById(person.getId());
   Assertions.assertNotNull(person);
   Assertions.assertNotNull(person.getAddress());
}

The values generated for my test run are visible below. As you see, the id field equals null. Other fields contain random values generated per the field type (String or int).

Person(id=null, name=ATDLCA, age=2619, gender=MALE, 
address=Address(country=FWOFRNT, city=AIRICCHGGG, street=ZZCIJDZ, houseNumber=5530, flatNumber=1671))

Let’s see how we can generate several objects with Instancio. Assuming we need 5 objects in the list for our test, we can do that in the following way. We will also set a constant value for the city fields inside the Address object. Then we would like to test the method for searching objects by the city name.

@Test
void addListAndGet() {
   final int numberOfObjects = 5;
   final String city = "Warsaw";
   List<Person> persons = Instancio.ofList(Person.class)
           .size(numberOfObjects)
           .set(Select.field(Address::getCity), city)
           .create();
   personService.addPersons(persons);
   persons = personService.findByCity(city);
   Assertions.assertEquals(numberOfObjects, persons.size());
}

Let’s take a look at the last example. The same as before, we are generating a list of objects – this time 100. We can easily specify the additional criteria for generated values. For example, I would like to set a value for the age field between 18 and 65.

@Test
void addGeneratorAndGet() {
   List<Person> persons = Instancio.ofList(Person.class)
            .size(100)
            .ignore(Select.field(Person::getId))
            .generate(Select.field(Person::getAge), 
                      gen -> gen.ints().range(18, 65))
            .create();
   personService.addPersons(persons);
   persons = personService.findAllGreaterThanAge(40);
   Assertions.assertTrue(persons.size() > 0);
}

That’s just a small set of customizations, that Instancio offers, for test data generation. You can read more about other options in their docs.

Datafaker

The next library we will discuss today is Datafaker. The purpose of this library is quite similar to the previous one. We need to generate random data. However, this time we need data that looks like real data. From my perspective, it is useful for demo presentations or examples running somewhere.

Datafaker creates fake data for your JVM programs within minutes, using our wide range of more than 100 data providers. This can be very helpful when generating test data to fill a database, generating data for a stress test, or anonymizing data from production services. Let’s include it in our dependencies.

<dependency>
  <groupId>net.datafaker</groupId>
  <artifactId>datafaker</artifactId>
  <version>1.7.0</version>
</dependency>

We will expand our sample model a little. Here’s a new class definition. The Contact class contains two fields email and phoneNumber. We will validate both these fields using the jakarta.validation module.

public class Contact {

   @Email
   private String email;
   @Pattern(regexp="\\d{2}-\\d{3}-\\d{2}-\\d{2}")
   private String phoneNumber;

   // getters and setters ...

}

Here’s a new version of our Person class that contains the Contant object instance:

public class Person {

   private Long id;
   private String name;
   private int age;
   private Gender gender;
   private Address address;
   @Valid
   private Contact contact;

   // getters and setters ...

}

Now, let’s generate fake data for the Person object. We can create localized data just by setting the Locale object in the Faker constructor (1). For me, it is Poland 🙂 There are a lot of providers for the standard values. To set email we need to use the Internet provider (2). There is a dedicated provider for generating phone numbers (3), addresses (4), and person names (5). You can see a full list of available providers here. After creating test data, we can run the test that adds a new Person verified on the server side.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PersonsControllerTests {

   @Autowired
   private TestRestTemplate restTemplate;

   @Test
   void add() {
      Faker faker = new Faker(Locale.of("pl")); // (1)
      Contact contact = new Contact();
      contact.setEmail(faker.internet().emailAddress()); // (2)
      contact.setPhoneNumber(faker.phoneNumber().cellPhone()); // (3)
      Address address = new Address();
      address.setCity(faker.address().city()); // (4)
      address.setCountry(faker.address().country());
      address.setStreet(faker.address().streetName());
      int number = Integer
         .parseInt(faker.address().streetAddressNumber());
      address.setHouseNumber(number);
      number = Integer.parseInt(faker.address().buildingNumber());
      address.setFlatNumber(number);
      Person person = new Person();
      person.setName(faker.name().fullName()); // (5)
      person.setContact(contact);
      person.setAddress(address);
      person.setGender(Gender.valueOf(
         faker.gender().binaryTypes().toUpperCase()));
      person.setAge(faker.number().numberBetween(18, 65));

      person = restTemplate
         .postForObject("/persons", person, Person.class);
      Assertions.assertNotNull(person);
      Assertions.assertNotNull(person.getId());
   }

}

Here’s the data generated during my test. I think you can find one inconsistency here (the country field) 😉

Person(id=null, name=Stefania Borkowski, age=51, gender=FEMALE, address=Address(country=Ekwador, city=Sępopol, street=al. Chudzik, houseNumber=882, flatNumber=318), contact=Contact{email='gilbert.augustyniak@gmail.com', phoneNumber='69-733-43-77'})

Sometimes you need to generate a more predictable random result. It’s possible to provide a seed value in the Faker constructor. When providing a seed, the instantiation of Fake objects will always happen in a predictable way, which can be handy for generating results multiple times. Here’s a new version of my Faker object declaration:

Faker faker = new Faker(Locale.of("pl"), new Random(0));

JPA Streamer

Our next library is related to JPA queries. If you like to use Java streams and you are building apps that interact with databases through JPA or Hibernate, the JPA Streamer library may be an interesting choice. It is a library for expressing JPA/Hibernate/Spring queries using standard Java streams. JPA Streamer instantly gives Java developers type-safe, expressive and intuitive means of obtaining data in database applications. Moreover, you can easily integrate it with Spring Boot and Quarkus. Firstly, let’s include JPA Streamer in our dependencies:

<dependency>
  <groupId>com.speedment.jpastreamer</groupId>
  <artifactId>jpastreamer-core</artifactId>
  <version>1.1.2</version>
</dependency>

If you want to integrate it with Spring Boot you need to add one additional dependency:

<dependency>
  <groupId>com.speedment.jpastreamer.integration.spring</groupId>
  <artifactId>spring-boot-jpastreamer-autoconfigure</artifactId>
  <version>1.1.2</version>
</dependency>

In order to test JPA Streamer, we need to create an example entities model.

@Entity
public class Employee {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Integer id;
   private String name;
   private String position;
   private int salary;
   @ManyToOne(fetch = FetchType.LAZY)
   private Department department;
   @ManyToOne(fetch = FetchType.LAZY)
   private Organization organization;

   // getters and setters ...

}

There are also two other entities: Organization and Department. Here are their definitions:

@Entity
public class Department {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Integer id;
   private String name;
   @OneToMany(mappedBy = "department")
   private Set<Employee> employees;
   @ManyToOne(fetch = FetchType.LAZY)
   private Organization organization;

   // getters and setters ...
}

@Entity
public class Organization {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Integer id;
   private String name;
   @OneToMany(mappedBy = "organization")
   private Set<Department> departments;
   @OneToMany(mappedBy = "organization")
   private Set<Employee> employees;

   // getters and setters ...
}

Now, we can prepare some queries using the Java streams pattern. In the following fragment of code, we are searching an entity by id and then joining two relations. By default, it is LEFT JOIN, but we can customize it when calling the joining() method. In the following fragment of code, we join Department and Organization, which are in @ManyToOne a relationship with the Employee entity. Then we filter the result, convert the object to the DTO and pick the first result.

@GetMapping("/{id}")
public EmployeeWithDetailsDTO findById(@PathVariable("id") Integer id) {
   return streamer.stream(of(Employee.class)
           .joining(Employee$.department)
           .joining(Employee$.organization))
        .filter(Employee$.id.equal(id))
        .map(EmployeeWithDetailsDTO::new)
        .findFirst()
        .orElseThrow();
}

Of course, we can call many other Java stream methods. In the following fragment of code, we count the number of employees assigned to the particular department.

@GetMapping("/{id}/count-employees")
public long getNumberOfEmployees(@PathVariable("id") Integer id) {
   return streamer.stream(Department.class)
         .filter(Department$.id.equal(id))
         .map(Department::getEmployees)
         .mapToLong(Set::size)
         .sum();
}

If you are looking for a detailed explanation and more examples with JPA Streamer you can my article dedicated to that topic.

Blaze Persistence

Blaze Persistence is another library from the JPA and Hibernate area. It allows you to write complex queries with a consistent builder API with rich Criteria API for JPA providers. That’s not all. You can also use the Entity-View module dedicated to DTO mapping. Of course, you can easily integrate with Spring Boot or Quarkus. If you want to use all Blaze Persistence modules in your app it is worth adding the dependencyManagement section in your Maven pom.xml:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.blazebit</groupId>
            <artifactId>blaze-persistence-bom</artifactId>
            <version>1.6.8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>    
    </dependencies>
</dependencyManagement>

Personally, I’m using Blaze Persistence for DTO mapping. Thanks to the integration with Spring Boot we can replace Spring Data Projections with Blaze Persistence Entity-Views. It will be especially useful for more advanced mappings since Blaze Persistence offers more features and better performance for that. You can read a detailed comparison in the following article. If we want to integrate Blaze Persistence Entity-Views with Spring Data we should add the following dependencies:

<dependency>
  <groupId>com.blazebit</groupId>
  <artifactId>blaze-persistence-integration-spring-data-2.7</artifactId>
</dependency>
<dependency>
  <groupId>com.blazebit</groupId>
  <artifactId>blaze-persistence-integration-hibernate-5.6</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>com.blazebit</groupId>
  <artifactId>blaze-persistence-entity-view-processor</artifactId>
</dependency>

Then, we need to create an interface with getters for mapped fields. It should be annotated with the @EntityView that refers to the target entity class. In the following example, we are mapping two entity fields firstName and lastName to the single fields inside the PersonDTO object. In order to map the entity’s primary key we should use the @IdMapping annotation.

@EntityView(Person.class)
public interface PersonView {

   @IdMapping
   Integer getId();
   void setId(Integer id);

   @Mapping("CONCAT(firstName,' ',lastName)")
   String getName();
   void setName(String name);

}

We can still take advantage of the Spring Data repository pattern. Our repository interface needs to extend the EntityViewRepository interface.

@Transactional(readOnly = true)
public interface PersonViewRepository 
    extends EntityViewRepository<PersonView, Integer> {

    PersonView findByAgeGreaterThan(int age);

}

We also need to provide some additional configuration and enable Blaze Persistence in the main or the configuration class:

@SpringBootApplication
@EnableBlazeRepositories
@EnableEntityViews
public class PersonApplication {

   public static void main(String[] args) {
      SpringApplication.run(PersonApplication.class, args);
   }

}

Hoverfly

Finally, the last of the Java libraries on my list – Hoverfly. To be more precise, we will use the Java version of the Hoverfly library documented here. It is a lightweight service virtualization tool that allows you to stub or simulate HTTP(S) services. Hoverfly Java is a native language binding that gives you an expressive API for managing Hoverfly in Java. It gives you a Hoverfly class which abstracts away the binary and API calls, a DSL for creating simulations, and a JUnit integration for using it within unit tests.

Ok, there are some other, similar libraries… but somehow I really like Hoverfly 🙂 It is a simple, lightweight library that may perform tests in different modes like simulation, spying, capture, or diffing. You can use Java DSL to build request matchers to response mappings. Let’s include the latest version of Hoverfly in the Maven dependencies:

<dependency>
  <groupId>io.specto</groupId>
  <artifactId>hoverfly-java-junit5</artifactId>
  <version>0.14.3</version>
</dependency>

Let’s assume we have the following method in our Spring @RestController. Before returning a ping response for itself, it calls another service under the address http://callme-service:8080/callme/ping.

@GetMapping("/ping")
public String ping() {
   String response = restTemplate
     .getForObject("http://callme-service:8080/callme/ping", 
                   String.class);
   LOGGER.info("Calling: response={}", response);
   return "I'm caller-service " + version + ". Calling... " + response;
}

Now, we will create the test for our controller. In order to use Hoverfly to intercept outgoing traffic we register HoverflyExtension (1). Then we may the Hoverfly object to create a request mather and simulate an HTTP response (2). The simulated response body is I'm callme-service v1.

@SpringBootTest(properties = {"VERSION = v2"}, 
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(HoverflyExtension.class) // (1)
public class CallerCallmeTest {
    
   @Autowired
   TestRestTemplate restTemplate;

   @Test
   void callmeIntegration(Hoverfly hoverfly) {
      hoverfly.simulate(
            dsl(service("http://callme-service:8080")
               .get("/callme/ping")
               .willReturn(success().body("I'm callme-service v1.")))
      ); // (2)
      String response = restTemplate
         .getForObject("/caller/ping", String.class);
      assertEquals("I'm caller-service v2. Calling... I'm callme-service v1.", response);
   }
}

We can easily customize Hovefly behavior with the @HoverflyConfig annotation. By default, Hoverfly works in proxy mode. Assuming we want it to act as a web server we need to set the property webserver to true (1). After that, it will listen for requests on localhost and the port indicated by the proxyPort property. In the next step, we will also enable Spring Cloud @LoadBalancedClient to configure a static list of target URLs instead of dynamic discovery (2). Finally, we can create a Hoverfly test. This time we are intercepting traffic from the web server listening on the localhost:8080 (3).

@SpringBootTest(webEnvironment = 
   SpringBootTest.WebEnvironment.RANDOM_PORT)
@HoverflyCore(config = 
   @HoverflyConfig(logLevel = LogLevel.DEBUG, 
                    webServer = true, 
                    proxyPort = 8080)) // (1)
@ExtendWith(HoverflyExtension.class)
@LoadBalancerClient(name = "account-service", 
                    configuration = AccountServiceConf.class) // (2)
public class GatewayTests {

    @Autowired
    TestRestTemplate restTemplate;

    @Test
    public void findAccounts(Hoverfly hoverfly) {
        hoverfly.simulate(dsl(
            service("http://localhost:8080")
                .andDelay(200, TimeUnit.MILLISECONDS).forAll()
                .get(any())
                .willReturn(success("[{\"id\":\"1\",\"number\":\"1234567890\",\"balance\":5000}]", "application/json")))); // (3)

        ResponseEntity<String> response = restTemplate
                .getForEntity("/account/1", String.class);
        Assertions.assertEquals(200, response.getStatusCodeValue());
        Assertions.assertNotNull(response.getBody());
    }
}

Here’s the load balancer client configuration created just for the test purpose.

class AccountServiceInstanceListSuppler implements 
    ServiceInstanceListSupplier {

    private final String serviceId;

    AccountServiceInstanceListSuppler(String serviceId) {
        this.serviceId = serviceId;
    }

    @Override
    public String getServiceId() {
        return serviceId;
    }

    @Override
    public Flux<List<ServiceInstance>> get() {
        return Flux.just(Arrays
                .asList(new DefaultServiceInstance(serviceId + "1", 
                        serviceId, 
                        "localhost", 8080, false)));
    }
}

Final Thoughts

As you probably figured out, I used all that Java libraries with Spring Boot apps. Although Spring Boot comes with a defined set of external libraries, sometimes we may need some add-ons. The Java libraries I presented are usually created to solve a single, particular problem like e.g. test data generation. It’s totally fine from my perspective. I hope you will find at least one position from my list useful in your projects.

The post Useful & Unknown Java Libraries appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2023/01/30/useful-unknown-java-libraries/feed/ 6 13954
Quick Guide to Microservices with Quarkus on Openshift https://piotrminkowski.com/2020/08/18/quick-guide-to-microservices-with-quarkus-on-openshift/ https://piotrminkowski.com/2020/08/18/quick-guide-to-microservices-with-quarkus-on-openshift/#comments Tue, 18 Aug 2020 14:18:04 +0000 https://piotrminkowski.wordpress.com/?p=7219 In this article I will show you how to use the Quarkus OpenShift module. Quarkus is a framework for building Java applications in times of microservices and serverless architectures. If you compare it with other frameworks like Spring Boot / Spring Cloud or Micronaut, the first difference is native support for running on Kubernetes or […]

The post Quick Guide to Microservices with Quarkus on Openshift appeared first on Piotr's TechBlog.

]]>
In this article I will show you how to use the Quarkus OpenShift module. Quarkus is a framework for building Java applications in times of microservices and serverless architectures. If you compare it with other frameworks like Spring Boot / Spring Cloud or Micronaut, the first difference is native support for running on Kubernetes or Openshift platforms. It is built on top of well-known Java standards like CDI, JAX-RS, and Eclipse MicroProfile which also distinguishes it from Spring Boot or Micronaut.
Some other features that may convince you to use Quarkus are extremely fast boot time, minimal memory footprint optimized for running in containers, and lower time-to-first-request. Also, even though it is a relatively new framework, it has a lot of extensions including support for Hibernate, Kafka, RabbitMQ, OpenApi, Vert.x, and many more.
I’m going to guide you through building microservices with Quarkus and running them on OpenShift 4. We will cover the following topics:

  • Building REST-based application with input validation
  • Communication between microservices with RestClient
  • Exposing health checks (liveness, readiness)
  • Exposing OpenAPI/Swagger documentation
  • Running applications on the local machine with Quarkus Maven plugin
  • Testing with JUnit and RestAssured
  • Deploying and running Quarkus applications on OpenShift using source-2-image

github-logo Source code

The source code of application is available on GitHub: https://github.com/piomin/sample-quarkus-microservices.git.

If you are interested in more materials related to Quarkus framework you can read my previous articles about it. In the article Guide to Quarkus with Kotlin I’m showing how to build a simple REST-based application written in Kotlin. In the article Guide to Quarkus on Kubernetes, I’m showing how to deploy it using Quarkus built-in support for Kubernetes.

1. Dependencies required for Quarkus OpenShift

When creating a new application you may execute a single Maven command that uses quarkus-maven-plugin. A list of dependencies should be declared in parameter -Dextensions.


mvn io.quarkus:quarkus-maven-plugin:1.7.0.Final:create \
    -DprojectGroupId=pl.piomin.services \
    -DprojectArtifactId=employee-service \
    -DclassName="pl.piomin.services.employee.controller.EmployeeController" \
    -Dpath="/employees" \
    -Dextensions="resteasy-jackson, hibernate-validator"

Here’s the structure of our pom.xml:

<properties>
   <quarkus.version>1.7.0.Final</quarkus.version>
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   <maven.compiler.source>11</maven.compiler.source>
   <maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>io.quarkus</groupId>
         <artifactId>quarkus-bom</artifactId>
         <version>${quarkus.version}</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
   </dependencies>
</dependencyManagement>
<build>
   <plugins>
      <plugin>
         <groupId>io.quarkus</groupId>
         <artifactId>quarkus-maven-plugin</artifactId>
         <version>${quarkus.version}</version>
         <executions>
            <execution>
               <goals>
                  <goal>build</goal>
               </goals>
            </execution>
         </executions>
      </plugin>
   </plugins>
</build>

For building a simple REST-based application with input validation we don’t need to include many modules. As you have probably noticed I declared just two extensions, which is the same as the following list of dependencies in Maven pom.xml:

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-hibernate-validator</artifactId>
</dependency>

2. Source code

What might be a bit surprising for Spring Boot or Micronaut users there is no main, runnable class with static method. A resource/controller class is defacto the main class. Quarkus resource/controller class and methods should be marked using annotations from javax.ws.rs library. Here’s the implementation of REST controller inside employee-service:

@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);
   }
	
}

We use CDI for dependency injection and SLF4J for logging. The controller class uses an in-memory repository bean for storing and retrieving data. Repository bean is annotated with CDI @ApplicationScoped and injected into the controller:

public class EmployeeRepository {

   private Set<Employee> employees = new HashSet<>();

   public EmployeeRepository() {
      add(new Employee(1L, 1L, "John Smith", 30, "Developer"));
      add(new Employee(1L, 1L, "Paul Walker", 40, "Architect"));
   }

   public Employee add(Employee employee) {
      employee.setId((long) (employees.size()+1));
      employees.add(employee);
      return employee;
   }
   
   public Employee findById(Long id) {
      Optional<Employee> employee = employees.stream()
            .filter(a -> a.getId().equals(id))
            .findFirst();
      if (employee.isPresent())
         return employee.get();
      else
         return null;
   }

   public Set<Employee> findAll() {
      return employees;
   }
   
   public Set<Employee> findByDepartment(Long departmentId) {
      return employees.stream()
            .filter(a -> a.getDepartmentId().equals(departmentId))
            .collect(Collectors.toSet());
   }
   
   public Set<Employee> findByOrganization(Long organizationId) {
      return employees.stream()
            .filter(a -> a.getOrganizationId().equals(organizationId))
            .collect(Collectors.toSet());
   }

}

And the last component is domain class with validation:

public class Employee {

   private Long id;
   @NotNull
   private Long organizationId;
   @NotNull
   private Long departmentId;
   @NotBlank
   private String name;
   @Min(1)
   @Max(100)
   private int age;
   @NotBlank
   private String position;
   
   // ... GETTERS AND SETTERS
   
}

3. Unit Testing

Unit testing with Quarkus is very simple. If you are testing REST-based web application you should include the following dependencies in your pom.xml:


<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-junit5</artifactId>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>io.rest-assured</groupId>
   <artifactId>rest-assured</artifactId>
   <scope>test</scope>
</dependency>

Let’s analyze the test class from organization-service (our another microservice along with employee-service and department-service). A test class should be annotated with @QuarkusTest. We may inject other beans via @Inject annotation. The rest is typical for JUnit and RestAssured – we are testing the API methods exposed by the controller. Because we are using an in-memory repository we don’t have to mock anything except inter-service communication (we discuss it later in that article). We have some positive scenarios for GET, POST methods and a single negative scenario that does not pass input validation (testInvalidAdd).

@QuarkusTest
public class OrganizationControllerTests {

   @Inject
   OrganizationRepository repository;

   @Test
   public void testFindAll() {
      given().when().get("/organizations")
	         .then()
			 .statusCode(200)
			 .body(notNullValue());
   }

   @Test
   public void testFindById() {
      Organization organization = new Organization("Test3", "Address3");
      organization = repository.add(organization);
      given().when().get("/organizations/{id}", organization.getId()).then().statusCode(200)
             .body("id", equalTo(organization.getId().intValue()))
             .body("name", equalTo(organization.getName()));
   }

   @Test
   public void testFindByIdWithDepartments() {
      given().when().get("/organizations/{id}/with-departments", 1L).then().statusCode(200)
             .body(notNullValue())
             .body("departments.size()", is(1));
   }

   @Test
   public void testAdd() {
      Organization organization = new Organization("Test5", "Address5");
      given().contentType("application/json").body(organization)
             .when().post("/organizations").then().statusCode(200)
             .body("id", notNullValue())
             .body("name", equalTo(organization.getName()));
   }

   @Test
   public void testInvalidAdd() {
      Organization organization = new Organization();
      given().contentType("application/json").body(organization)
	         .when()
			 .post("/organizations")
			 .then()
			 .statusCode(400);
   }

}

4. Inter-service communication

Since Quarkus is dedicated to running on Kubernetes it does not provide any built-in support for third-party service discovery (for example through Consul or Netflix Eureka) and HTTP client integrated with this discovery. However, it provides dedicated client support for REST communication. To use it we first need to include the following dependency:


<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-rest-client</artifactId>
</dependency>

Quarkus provides declarative REST client based on MicroProfile REST Client. You need to create an interface with the required methods and annotate it with @RegisterRestClient. Other annotations are pretty the same as on the server-side. Since you use @RegisterRestClient for marking Quarkus know that this interface is meant to be available for CDI injection as a REST Client.

@Singleton
@Path("/departments")
@RegisterRestClient
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);
   
}

Now, let’s take a look at the controller class inside the organization-service. Together with @Inject we need to use @RestClient annotation to inject REST client bean properly. After that, you can use interface methods to call endpoints exposed by other services.

@Path("/organizations")
@Produces(MediaType.APPLICATION_JSON)
public class OrganizationController {

   private static final Logger LOGGER = LoggerFactory.getLogger(OrganizationController.class);
   
   @Inject
   OrganizationRepository repository;
   @Inject
   @RestClient
   DepartmentClient departmentClient;
   @Inject
   @RestClient
   EmployeeClient employeeClient;
   
   // ... OTHER FIND METHODS

   @Path("/{id}/with-departments")
   @GET
   public Organization findByIdWithDepartments(@PathParam("id") Long id) {
      LOGGER.info("Organization find: id={}", id);
      Organization organization = repository.findById(id);
      organization.setDepartments(departmentClient.findByOrganization(organization.getId()));
      return organization;
   }
   
   @Path("/{id}/with-departments-and-employees")
   @GET
   public Organization findByIdWithDepartmentsAndEmployees(@PathParam("id") Long id) {
      LOGGER.info("Organization find: id={}", id);
      Organization organization = repository.findById(id);
      organization.setDepartments(departmentClient.findByOrganizationWithEmployees(organization.getId()));
      return organization;
   }
   
   @Path("/{id}/with-employees")
   @GET
   public Organization findByIdWithEmployees(@PathParam("id") Long id) {
      LOGGER.info("Organization find: id={}", id);
      Organization organization = repository.findById(id);
      organization.setEmployees(employeeClient.findByOrganization(organization.getId()));
      return organization;
   }
   
}

The last missing thing required for communication is the addresses of target services. We may provide them using field baseUri of @RegisterRestClient annotation. However, it seems that a better solution would be to place them inside application.properties. The name of the property needs to contain the fully qualified name of the client interface and suffix mp-rest/url. The address used for communication in development mode is different than in production mode when the application is deployed on Kubernetes or OpenShift. That’s why I’m using prefix %dev in the name of the property setting target URL.


%dev.pl.piomin.services.organization.client.DepartmentClient/mp-rest/url=http://localhost:8090
%dev.pl.piomin.services.organization.client.EmployeeClient/mp-rest/url=http://localhost:8080

I have already mentioned unit testing and inter-service communication in the previous section. To test the API method that communicates with other applications we need to mock the REST client. Here’s the sample of mock created for DepartmentClient. It should be visible only during the tests, so we have to place it inside src/test/java. If we annotate it with @Mock and @RestClient Quarkus automatically use this bean by default instead of declarative REST client-defined inside src/main/java.

@Mock
@ApplicationScoped
@RestClient
public class MockDepartmentClient implements DepartmentClient {

    @Override
    public List<Department> findByOrganization(Long organizationId) {
        return Collections.singletonList(new Department("Test1"));
    }

    @Override
    public List<Department> findByOrganizationWithEmployees(Long organizationId) {
        return null;
    }

}

5. Monitoring and Documentation

We can easily expose health checks or API documentation with Quarkus. API documentation is built using OpenAPI/Swagger. Quarkus leverages libraries available within the project SmallRye. We should include the following dependencies to our pom.xml:


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

We can define two types of health checks: readiness and liveness. There are available under /health/ready and /health/live context paths. To expose them outside application we need to define a bean that implements MicroProfile HealthCheck interface. Readiness endpoint should be annotated with @Readiness, while liveness with @Liveness.

@ApplicationScoped
@Readiness
public class ReadinessHealthcheck implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.named("Employee Health Check").up().build();
    }

}

To enable Swagger documentation we don’t need to do anything more than adding a dependency. Quarkus also provides a built-in UI for Swagger. By default it is enabled on development mode, so if you are willing to use on the production you should add the line quarkus.swagger-ui.always-include=true to your application.properties file. Now, if run the application employee-service locally in development mode by executing Maven command mvn compile quarkus:dev you may view API specification available under URL http://localhost:8080/swagger-ui.

swagger

Here’s my log from application startup. It prints a listening port and list of loaded extensions.

quarkus-startup

6. Running Quarkus Microservices on the Local Machine

Because we would like to run more than one application on the same machine we need to override their default HTTP listening port. While employee-service is still running on the default 8080 port, other microservices use different ports as shown below.

department-service:
port-department

organization-service:
port-organization

Let’s test inter-service communication from Swagger UI. I called endpoint GET /organizations/{id}/with-departments that calls endpoint GET /departments/organization/{organizationId} exposed by department-service. The result is visible on the below.

quarkus-communication

7. Running Quarkus on OpenShift

We have already finished the implementation of our sample microservices architecture and run them on the local machine. Now, we can proceed to the last step and deploy these applications on OpenShift or Minishift. We have some different approaches when deploying the Quarkus application on OpenShift. Today I’ll show you leverage the S2I build mechanism for that.
We are going to use Quarkus GraalVM Native S2I Builder. It is available on quai.io as quarkus/ubi-quarkus-native-s2i. I’m using the Openshift 4 cluster running on Azure. You try it as well on the local version OpenShift 3 – Minishift. Before deploying our applications we need to start Minishift. Following Quarkus documentation GraalVM-based native build consumes much memory and CPU, so you should set 6GB and 4 cores for Minishift.


$ minishift start --vm-driver=virtualbox --memory=6G --cpus=4

Also, we need to modify the source code of our application a little. As you probably remember we used JDK 11 for running them locally. We also need to include a declaration of native profile as shown below:

<properties>
   <quarkus.version>1.7.0.Final</quarkus.version>
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   <maven.compiler.source>1.8</maven.compiler.source>
   <maven.compiler.target>1.8</maven.compiler.target>
</properties>
...
<profiles>
   <profile>
      <id>native</id>
      <activation>
         <property>
            <name>native</name>
         </property>
      </activation>
      <build>
         <plugins>
            <plugin>
               <groupId>io.quarkus</groupId>
               <artifactId>quarkus-maven-plugin</artifactId>
               <version>${quarkus.version}</version>
               <executions>
                  <execution>
                     <goals>
                        <goal>native-image</goal>
                     </goals>
                     <configuration>
                        <enableHttpUrlHandler>true</enableHttpUrlHandler>
                     </configuration>
                  </execution>
               </executions>
            </plugin>
            <plugin>
               <artifactId>maven-failsafe-plugin</artifactId>
               <version>2.22.1</version>
               <executions>
                  <execution>
                     <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                     </goals>
                     <configuration>
                        <systemProperties>
                           <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                        </systemProperties>
                     </configuration>
                  </execution>
               </executions>
            </plugin>
         </plugins>
      </build>
   </profile>
</profiles>

Two other changes should be performed inside application.properties file. We don’t have to override port number, since OpenShift dynamically assigns virtual IP for every pod. An inter-service communication is realized via OpenShift discovery, so we just need to set the name of service instead of the localhost. It can be set in the default profile for the property since properties with %dev prefix are used in development mode.

quarkus.swagger-ui.always-include=true
pl.piomin.services.organization.client.DepartmentClient/mp-rest/url=http://department:8080
pl.piomin.services.organization.client.EmployeeClient/mp-rest/url=http://employee:8080

Finally we may deploy our applications on OpenShift. To do that you should execute the following commands using your oc client:

$ oc new-app quay.io/quarkus/ubi-quarkus-native-s2i:20.1.0-java11~https://github.com/piomin/sample-quarkus-microservices.git --context-dir=employee-service --name=employee
$ oc new-app quay.io/quarkus/ubi-quarkus-native-s2i:20.1.0-java11~https://github.com/piomin/sample-quarkus-microservices.git --context-dir=department-service --name=department
$ oc new-app quay.io/quarkus/ubi-quarkus-native-s2i:20.1.0-java11~https://github.com/piomin/sample-quarkus-microservices.git --context-dir=organization-service --name=organization

As you can see the repository with applications source code is available on my GitHub account under address https://github.com/piomin/sample-quarkus-microservices.git. Because all the applications are stored within a single repository we need to define a parameter context-dir for every single deployment.
I was quite disappointed. Since we are using GraalVM for compilation the memory consumption of the build is pretty large. The whole build process takes around 10 minutes.

quarkus-microservices-openshift-build

Here’s the list of performed builds.

quarkus-microservices-openshift-build-list

Although a build process consumes much memory, the memory usage of Quarkus applications compiled using GraalVM is just amazing.

quarkus-microservices-openshift-pods

To execute some test calls we need to expose applications outside the OpenShift cluster.

$ oc expose svc employee
$ oc expose svc department
$ oc expose svc organization

In my OpenShift cluster they will be available under the address, for example http://department-quarkus.apps.np9zir0r.westeurope.aroapp.io. You can run Swagger UI by calling /swagger-ui context path on every single application.

quarkus-microservices-openshift-routes

The post Quick Guide to Microservices with Quarkus on Openshift appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2020/08/18/quick-guide-to-microservices-with-quarkus-on-openshift/feed/ 2 7219
Part 1: Testing Kafka Microservices With Micronaut https://piotrminkowski.com/2019/10/09/part-1-testing-kafka-microservices-with-micronaut/ https://piotrminkowski.com/2019/10/09/part-1-testing-kafka-microservices-with-micronaut/#respond Wed, 09 Oct 2019 09:08:26 +0000 https://piotrminkowski.wordpress.com/?p=7305 I have already described how to build microservices architecture entirely based on message-driven communication through Apache Kafka in one of my previous articles Kafka In Microservices With Micronaut. As you can see in the article title the sample applications and integration with Kafka has been built on top of Micronaut Framework. I described some interesting […]

The post Part 1: Testing Kafka Microservices With Micronaut appeared first on Piotr's TechBlog.

]]>
I have already described how to build microservices architecture entirely based on message-driven communication through Apache Kafka in one of my previous articles Kafka In Microservices With Micronaut. As you can see in the article title the sample applications and integration with Kafka has been built on top of Micronaut Framework. I described some interesting features of Micronaut, that can be used for building message-driven microservices, but I didn’t specifically write anything about testing. In this article I’m going to show you example of testing your Kafka microservices using Micronaut Test core features (Component Tests), Testcontainers (Integration Tests) and Pact (Contract Tests).

Generally, automated testing is one of the biggest challenges related to microservices architecture. Therefore the most popular microservice frameworks like Micronaut or Spring Boot provide some useful features for that. There are also some dedicated tools which help you to use Docker containers in your tests or provide mechanisms for verifying the contracts between different applications. For the purpose of current article demo applications I’m using the same repository as for the previous article: https://github.com/piomin/sample-kafka-micronaut-microservices.git.

Sample Architecture

The architecture of sample applications has been described in the previous article but let me perform a quick recap. We have 4 microservices: order-service, trip-service, driver-service and passenger-service. The implementation of these applications is very simple. All of them have in-memory storage and connect to the same Kafka instance.
A primary goal of our system is to arrange a trip for customers. The order-service application also acts as a gateway. It is receiving requests from customers, saving history and sending events to orders topic. All the other microservices are listening on this topic and processing orders sent by order-service. Each microservice has its own dedicated topic, where it sends events with information about changes. Such events are received by some other microservices. The architecture is presented in the picture below.

micronaut-kafka-1

Embedded Kafka – Component Testing with Micronaut

After a short description of the architecture we may proceed to the key point of this article – testing. Micronaut allows you to start an embedded Kafka instance for the purpose of testing. To do that you should first include the following dependencies to your Maven pom.xml:

<dependency>
   <groupId>org.apache.kafka</groupId>
   <artifactId>kafka-clients</artifactId>
   <version>2.3.0</version>
   <classifier>test</classifier>
</dependency>
<dependency>
   <groupId>org.apache.kafka</groupId>
   <artifactId>kafka_2.12</artifactId>
   <version>2.3.0</version>
</dependency>
<dependency>
   <groupId>org.apache.kafka</groupId>
   <artifactId>kafka_2.12</artifactId>
   <version>2.3.0</version>
   <classifier>test</classifier>
</dependency>

To enable embedded Kafka for a test class we have to set property kafka.embedded.enabled to true. Because I have run Kafka on Docker container, which is by default available on address 192.168.99.100 I also need to change dynamically the value of property kafka.bootstrap.servers to localhost:9092 for a given test. The test implementation class uses embedded Kafka for testing three basic scenarios for order-service: sending orders with new trip, and receiving orders for trip cancellation and completion from other microservices. Here’s the full code of my OrderKafkaEmbeddedTest

@MicronautTest
@Property(name = "kafka.embedded.enabled", value = "true")
@Property(name = "kafka.bootstrap.servers", value = "localhost:9092")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class OrderKafkaEmbeddedTest {

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

    @Inject
    OrderClient client;
    @Inject
    OrderInMemoryRepository repository;
    @Inject
    OrderHolder orderHolder;
    @Inject
    KafkaEmbedded kafkaEmbedded;

    @BeforeAll
    public void init() {
        LOGGER.info("Topics: {}", kafkaEmbedded.getKafkaServer().get().zkClient().getAllTopicsInCluster());
    }

    @Test
    @org.junit.jupiter.api.Order(1)
    public void testAddNewTripOrder() throws InterruptedException {
        Order order = new Order(OrderType.NEW_TRIP, 1L, 50, 30);
        order = repository.add(order);
        client.send(order);
        Order orderSent = waitForOrder();
        Assertions.assertNotNull(orderSent);
        Assertions.assertEquals(order.getId(), orderSent.getId());
    }

    @Test
    @org.junit.jupiter.api.Order(2)
    public void testCancelTripOrder() throws InterruptedException {
        Order order = new Order(OrderType.CANCEL_TRIP, 1L, 50, 30);
        client.send(order);
        Order orderReceived = waitForOrder();
        Optional<Order> oo = repository.findById(1L);
        Assertions.assertTrue(oo.isPresent());
        Assertions.assertEquals(OrderStatus.REJECTED, oo.get().getStatus());
    }

    @Test
    @org.junit.jupiter.api.Order(3)
    public void testPaymentTripOrder() throws InterruptedException {
        Order order = new Order(OrderType.PAYMENT_PROCESSED, 1L, 50, 30);
        order.setTripId(1L);
        order = repository.add(order);
        client.send(order);
        Order orderSent = waitForOrder();
        Optional<Order> oo = repository.findById(order.getId());
        Assertions.assertTrue(oo.isPresent());
        Assertions.assertEquals(OrderStatus.COMPLETED, oo.get().getStatus());
    }

    private Order waitForOrder() throws InterruptedException {
        Order orderSent = null;
        for (int i = 0; i < 10; i++) {
            orderSent = orderHolder.getCurrentOrder();
            if (orderSent != null)
                break;
            Thread.sleep(1000);
        }
        orderHolder.setCurrentOrder(null);
        return orderSent;
    }

}

At that stage some things require clarification – especially the mechanism of verifying sending and receiving messages. I’ll describe it in the example of driver-service. When a message is incoming to the order topic it is received by OrderListener, which is annotated with @KafkaListener as shown below. It gets the order type and forwards the NEW_TRIP request to DriverService bean.

@KafkaListener(groupId = "driver")
public class OrderListener {

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

    private DriverService service;

    public OrderListener(DriverService service) {
        this.service = service;
    }

    @Topic("orders")
    public void receive(@Body Order order) {
        LOGGER.info("Received: {}", order);
        switch (order.getType()) {
            case NEW_TRIP -> service.processNewTripOrder(order);
        }
    }
}

The DriverService is processing order. It is trying to find the driver located closest to the customer, changing found driver’s status to unavailable and sending events with change with the current driver state.

@Singleton
public class DriverService {

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

    private DriverClient client;
    private OrderClient orderClient;
    private DriverInMemoryRepository repository;

    public DriverService(DriverClient client, OrderClient orderClient, DriverInMemoryRepository repository) {
        this.client = client;
        this.orderClient = orderClient;
        this.repository = repository;
    }

    public void processNewTripOrder(Order order) {
        LOGGER.info("Processing: {}", order);
        Optional<Driver> driver = repository.findNearestDriver(order.getCurrentLocationX(), order.getCurrentLocationY());
        if (driver.isPresent()) {
            Driver driverLocal = driver.get();
            driverLocal.setStatus(DriverStatus.UNAVAILABLE);
            repository.updateDriver(driverLocal);
            client.send(driverLocal, String.valueOf(order.getId()));
            LOGGER.info("Message sent: {}", driverLocal);
        }
    }
   
   // OTHER METHODS ...
}

To verify that a final message with change notification has been sent to the drivers topic we have to create our own listener for the test purposes. It receives the message and writes it in @Singleton holder class which is then accessed by a single-thread test class. The described process is visualized in the picture below.
kafka-micronaut-testing-1.png
Here’s the implementation of test listener which is responsible just for receiving the message sent to drivers topic and writing it to DriverHolder bean.

@KafkaListener(groupId = "driverTest")
public class DriverConfirmListener {

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

   @Inject
   DriverHolder driverHolder;

   @Topic("orders")
   public void receive(@Body Driver driver) {
      LOGGER.info("Confirmed: {}", driver);
      driverHolder.setCurrentDriver(driver);
   }

}

Here’s the implementation of DriverHolder class.

@Singleton
public class DriverHolder {

   private Driver currentDriver;

   public Driver getCurrentDriver() {
      return currentDriver;
   }

   public void setCurrentDriver(Driver currentDriver) {
      this.currentDriver = currentDriver;
   }

}

No matter if you are using embedded Kafka, Testcontainers or just manually started a Docker container you can use the verification mechanism described above.

Kafka with Testcontainers

We will use the Testcontainers framework for running Docker containers of Zookeeper and Kafka during JUnit tests. Testcontainers is a Java library that provides lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. To use it in your project together with JUnit 5, which is already used for our sample Micronaut application, you have to add the following dependencies to your Maven pom.xml:

<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>kafka</artifactId>
   <version>1.12.2</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>junit-jupiter</artifactId>
   <version>1.12.2</version>
   <scope>test</scope>
</dependency>

The declared library org.testcontainers:kafka:1.12.2 provides KafkaContainer class that allows to define and start a Kafka container with embedded Zookeeper in your tests. However, I decided to use GenericContainer class and run two containers wurstmeister/zookeeper and wurstmeister/kafka. Because Kafka needs to communicate with Zookeeper both containers should be run in the same network. We will also have to override Zookeeper container’s name and host name to allow Kafka to call it by the hostname.
When running a Kafka container we need to set some important environment variables. Variable KAFKA_ADVERTISED_HOST_NAME sets the hostname under which Kafka is visible for external client and KAFKA_ZOOKEEPER_CONNECT Zookeeper lookup address. Although it is not recommended we should disable dynamic exposure port generation by setting static port number equal to the container binding port 9092. That helps us to avoid some problems with setting Kafka advertised port and injecting it into Micronaut configuration.

@MicronautTest
@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class OrderKafkaContainerTest {

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

    static Network network = Network.newNetwork();

   @Container
   public static final GenericContainer ZOOKEEPER = new GenericContainer("wurstmeister/zookeeper")
      .withCreateContainerCmdModifier(it -> ((CreateContainerCmd) it).withName("zookeeper").withHostName("zookeeper"))
      .withExposedPorts(2181)
      .withNetworkAliases("zookeeper")
      .withNetwork(network);

   @Container
   public static final GenericContainer KAFKA_CONTAINER = new GenericContainer("wurstmeister/kafka")
      .withCreateContainerCmdModifier(it -> ((CreateContainerCmd) it).withName("kafka").withHostName("kafka")
         .withPortBindings(new PortBinding(Ports.Binding.bindPort(9092), new ExposedPort(9092))))
      .withExposedPorts(9092)
      .withNetworkAliases("kafka")
      .withEnv("KAFKA_ADVERTISED_HOST_NAME", "192.168.99.100")
      .withEnv("KAFKA_ZOOKEEPER_CONNECT", "zookeeper:2181")
      .withNetwork(network);
      
   // TESTS ...
   
}

The test scenarios may be the same as for embedded Kafka or we may attempt to define some more advanced integration tests. To do that we first create a Docker image of every microservice during the build. We can use io.fabric8:docker-maven-plugin for that. Here’s the example for driver-service.

<plugin>
   <groupId>io.fabric8</groupId>
   <artifactId>docker-maven-plugin</artifactId>
   <version>0.31.0</version>
   <configuration>
      <images>
         <image>
            <name>piomin/driver-service:${project.version}</name>
            <build>
               <dockerFile>${project.basedir}/Dockerfile</dockerFile>
               <tags>
                  <tag>latest</tag>
                  <tag>${project.version}</tag>
               </tags>
            </build>
         </image>
      </images>
   </configuration>
   <executions>
      <execution>
         <id>start</id>
         <phase>pre-integration-test</phase>
         <goals>
            <goal>build</goal>
            <goal>start</goal>
         </goals>
      </execution>
      <execution>
         <id>stop</id>
         <phase>post-integration-test</phase>
         <goals>
            <goal>stop</goal>
         </goals>
      </execution>
   </executions>
</plugin>

If we have a Docker image of every microservice we can easily run it using Testcontainers during our integration tests. In the fragment of test class visible below I’m running the container with driver-service in addition to Kafka and Zookeeper containers. The test is implemented inside order-service. We are building the same scenario as in the test with embedded Kafka – sending the NEW_TRIP order. But this time we are verifying if the message has been received and processed by the driver-service. This verification is performed by listening for notification events sent by driver-service started on Docker container to the drivers topic. Normally, order-service does not listen for messages incoming to drivers topic, but we created such integration just for the integration test purpose.

@Container
public static final GenericContainer DRIVER_CONTAINER = new GenericContainer("piomin/driver-service")
   .withNetwork(network);

@Inject
OrderClient client;
@Inject
OrderInMemoryRepository repository;
@Inject
DriverHolder driverHolder;

@Test
@org.junit.jupiter.api.Order(1)
public void testNewTrip() throws InterruptedException {
   Order order = new Order(OrderType.NEW_TRIP, 1L, 50, 30);
   order = repository.add(order);
   client.send(order);
   Driver driverReceived = null;
   for (int i = 0; i < 10; i++) {
      driverReceived = driverHolder.getCurrentDriver();
      if (driverReceived != null)
         break;
      Thread.sleep(1000);
   }
   driverHolder.setCurrentDriver(null);
   Assertions.assertNotNull(driverReceived);
}

Summary

In this article, I have described an approach to component testing with embedded Kafka, and Micronaut, and also integration tests with Docker and Testcontainers. This is the first part of the article, in the second I’m going to show you how to build contract tests for Micronaut applications with Pact.

The post Part 1: Testing Kafka Microservices With Micronaut appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2019/10/09/part-1-testing-kafka-microservices-with-micronaut/feed/ 0 7305
Microservices Integration Tests with Hoverfly and Testcontainers https://piotrminkowski.com/2019/02/06/microservices-integration-tests-with-hoverfly-and-testcontainers/ https://piotrminkowski.com/2019/02/06/microservices-integration-tests-with-hoverfly-and-testcontainers/#respond Wed, 06 Feb 2019 13:09:59 +0000 https://piotrminkowski.wordpress.com/?p=6999 Building good integration tests of a system consisting of several microservices may be quite a challenge. Today I’m going to show you how to use such tools like Hoverfly and Testcontainers to implement such tests. I have already written about Hoverfly in my previous articles, as well as about Testcontainers. If you are interested in […]

The post Microservices Integration Tests with Hoverfly and Testcontainers appeared first on Piotr's TechBlog.

]]>
Building good integration tests of a system consisting of several microservices may be quite a challenge. Today I’m going to show you how to use such tools like Hoverfly and Testcontainers to implement such tests. I have already written about Hoverfly in my previous articles, as well as about Testcontainers. If you are interested in some intro to these framework you may take a look on the following articles:

Today we will consider the system consisting of three microservices, where each microservice is developed by the different team. One of these microservices trip-management is integrating with two others: driver-management and passenger-management. The question is how to organize integration tests under these assumptions. In that case we can use one of the interesting features provided by Hoverfly – an ability to run it as a remote proxy. What does it mean in practice? It is illustrated in the picture below. The same external instance of Hoverfly proxy is shared between all microservices during JUnit tests. Microservice driver-management and passenger-management are testing their own methods exposed for use by trip-management, but all the requests are sent through Hoverfly remote instance acts as a proxy. Hoverfly will capture all the requests and responses sent during the tests. On the other hand trip-management is also testing its methods, but the communication with other microservices is simulated by the remote Hoverfly instance based on previously captured HTTP traffic.

hoverfly-test-1.png

We will use Docker for running remote instances of Hoverfly proxy. We will also use Docker images of microservices during the tests. That’s why we need the Testcontainers framework, which is responsible for running an application container before starting integration tests. So, the first step is to build a Docker image of driver-management and passenger-management microservices.

1. Building Docker Image

Assuming you have successfully installed Docker on your machine, and you have set environment variables DOCKER_HOST and DOCKER_CERT_PATH, you may use io.fabric:docker-maven-plugin for it. It is important to execute the build goal of that plugin just after package Maven phase, but before integration-test phase. Here’s the appropriate configuration inside Maven pom.xml.

<plugin>
   <groupId>io.fabric8</groupId>
   <artifactId>docker-maven-plugin</artifactId>
   <configuration>
      <images>
         <image>
            <name>piomin/driver-management</name>
            <alias>dockerfile</alias>
            <build>
               <dockerFileDir>${project.basedir}</dockerFileDir>
            </build>
         </image>
      </images>
   </configuration>
   <executions>
      <execution>
         <phase>pre-integration-test</phase>
         <goals>
            <goal>build</goal>
         </goals>
      </execution>
   </executions>
</plugin>

2. Application Integration Tests

Our integration tests should be run during the integration-test phase, so they must not be executed during test, before building application fat jar and Docker image. Here’s the appropriate configuration with maven-surefire-plugin.

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-surefire-plugin</artifactId>
   <configuration>
      <excludes>
         <exclude>pl.piomin.services.driver.contract.DriverControllerIntegrationTests</exclude>
      </excludes>
   </configuration>
   <executions>
      <execution>
         <id>integration-test</id>
         <goals>
            <goal>test</goal>
         </goals>
         <phase>integration-test</phase>
         <configuration>
            <excludes>
               <exclude>none</exclude>
            </excludes>
            <includes>
               <include>pl.piomin.services.driver.contract.DriverControllerIntegrationTests</include>
            </includes>
         </configuration>
      </execution>
   </executions>
</plugin>

3. Running Hoverfly

Before running any tests we need to start an instance of Hoverfly in proxy mode. To achieve it we use Hoverfly Docker image. Because Hoverfly has to forward requests to the downstream microservices by host name, we create Docker network and then run Hoverfly in this network.

$ docker network create tests
$ docker run -d --name hoverfly -p 8500:8500 -p 8888:8888 --network tests spectolabs/hoverfly

Hoverfly proxy is now available for me (I’m using Docker Toolbox) under address 192.168.99.100:8500. We can also take a look at the admin web console available under address http://192.168.99.100:8888. Under that address you can also access HTTP API, what is described later in the next section.

4. Including test dependencies

To enable Hoverfly and Testcontainers for our test we first need to include some dependencies to Maven pom.xml. Our sample applications are built on top of Spring Boot, so we also include the Spring Test project.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>testcontainers</artifactId>
   <version>1.10.6</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>io.specto</groupId>
   <artifactId>hoverfly-java</artifactId>
   <version>0.11.1</version>
   <scope>test</scope>
</dependency>

5. Building integration tests on the provider site

Now, we can finally proceed to JUnit test implementation. Here’s the full source code of the test for driver-management microservice, but some things need to be explained. Before running our test methods we first start a Docker container of application using Testcontainers. We use GenericContainer annotated with @ClassRule for that. Testcontainers provides api for interaction with containers, so we can easily set target Docker network and container hostname. We will also wait until the application container is ready for use by calling method waitingFor on GenericContainer.
The next step is to enable the Hoverfly rule for our test. We will run it in capture mode. By default Hoverfly is trying to start a local proxy instance, that’s why we provide remote addresses of an existing instance already started using Docker container.
The tests are pretty simple. We will call endpoints using Spring TestRestTemplate. Because the request must finally be proxied to the application container we use its hostname as the target address. The whole traffic is captured by Hoverfly.

public class DriverControllerIntegrationTests {

    private TestRestTemplate template = new TestRestTemplate();

    @ClassRule
    public static GenericContainer appContainer = new GenericContainer<>("piomin/driver-management")
            .withCreateContainerCmdModifier(cmd -> cmd.withName("driver-management").withHostName("driver-management"))
            .withNetworkMode("tests")
            .withNetworkAliases("driver-management")
            .withExposedPorts(8080)
            .waitingFor(Wait.forHttp("/drivers"));

    @ClassRule
    public static HoverflyRule hoverflyRule = HoverflyRule
            .inCaptureMode("driver.json", HoverflyConfig.remoteConfigs().host("192.168.99.100"))
            .printSimulationData();

    @Test
    public void testFindNearestDriver() {
        Driver driver = template.getForObject("http://driver-management:8080/drivers/{locationX}/{locationY}", Driver.class, 40, 20);
        Assert.assertNotNull(driver);
        driver = template.getForObject("http://driver-management:8080/drivers/{locationX}/{locationY}", Driver.class, 10, 20);
        Assert.assertNotNull(driver);
    }

    @Test
    public void testUpdateDriver() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        DriverInput input = new DriverInput();
        input.setId(2L);
        input.setStatus(DriverStatus.UNAVAILABLE);
        HttpEntity<DriverInput> entity = new HttpEntity<>(input, headers);
        template.put("http://driver-management:8080/drivers", entity);
        input.setId(1L);
        input.setStatus(DriverStatus.AVAILABLE);
        entity = new HttpEntity<>(input, headers);
        template.put("http://driver-management:8080/drivers", entity);
    }

}

Now, you can execute the tests during application build using mvn clean verify command. The sample application source code is available on GitHub in repository sample-testing-microservices under branch remote.

6. Building integration tests on the consumer site

In the previous we have discussed the integration tests implemented on the consumer site. There are two microservices driver-management and passenger-management, that expose endpoints invoked by the third microservice trip-management. The traffic generated during the tests has already been captured by Hoverfly. It is a very important thing in that sample, because each time you will build the newest version of microservice Hoverfly is refreshing the structure of previously recorded requests. Now, if we run the tests for a consumer application (trip-management) it fully bases on the newest version of requests generated during tests by microservices on the provider site. You can check out the list of all requests captured by Hoverfly by calling endpoint http://192.168.99.100:8888/api/v2/simulation.
Here are the integration tests implemented inside trip-management. They also use a remote Hoverfly proxy instance. The only difference is in running mode, which is simulation. It tries to simulate requests sent to driver-management and passenger-management basing on the traffic captured by Hoverfly.

@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TripIntegrationTests {

    ObjectMapper mapper = new ObjectMapper();

    @ClassRule
    public static HoverflyRule hoverflyRule = HoverflyRule
            .inSimulationMode(HoverflyConfig.remoteConfigs().host("192.168.99.100"))
            .printSimulationData();

    @Autowired
    MockMvc mockMvc;

    @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")));
    }

}

Now, you can run command mvn clean verify on the root module. It runs the tests in the following order: driver-management, passenger-management and trip-management.

hoverfly-test-3

The post Microservices Integration Tests with Hoverfly and Testcontainers appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2019/02/06/microservices-integration-tests-with-hoverfly-and-testcontainers/feed/ 0 6999
Testing Spring Boot Integration with Vault and Postgres using Testcontainers Framework https://piotrminkowski.com/2019/01/31/testing-spring-boot-integration-with-vault-and-postgres-using-testcontainers-framework/ https://piotrminkowski.com/2019/01/31/testing-spring-boot-integration-with-vault-and-postgres-using-testcontainers-framework/#respond Thu, 31 Jan 2019 14:07:48 +0000 https://piotrminkowski.wordpress.com/?p=6992 I have already written many articles, where I was using Docker containers for running some third-party solutions integrated with my sample applications. Building integration tests for such applications may not be an easy task without Docker containers. Especially, if our application integrates with databases, message brokers or some other popular tools. If you are planning […]

The post Testing Spring Boot Integration with Vault and Postgres using Testcontainers Framework appeared first on Piotr's TechBlog.

]]>
I have already written many articles, where I was using Docker containers for running some third-party solutions integrated with my sample applications. Building integration tests for such applications may not be an easy task without Docker containers. Especially, if our application integrates with databases, message brokers or some other popular tools. If you are planning to build such integration tests you should definitely take a look on Testcontainers (https://www.testcontainers.org/).
Testcontainers is a Java library that supports JUnit tests, providing fast and lightweight way for running instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. It provides modules for the most popular relational and NoSQL databases like Postgres, MySQL, Cassandra or Neo4j. It also allows them to run popular products like Elasticsearch, Kafka, Nginx or HashiCorp’s Vault. Today I’m going to show you a more advanced sample of JUnit tests that use Testcontainers to check out an integration between Spring Boot/Spring Cloud application, Postgres database and Vault. For the purposes of that example we will use the case described in one of my previous articles Secure Spring Cloud Microservices with Vault and Nomad. Let us recall that use case.
I described there how to use a very interesting Vault feature called secret engines for generating database user credentials dynamically. I used the Spring Cloud Vault module in my Spring Boot application to automatically integrate with that feature of Vault. The implemented mechanism is pretty easy. The application calls Vault secret engine before it tries to connect to Postgres database on startup. Vault is integrated with Postgres via a secret engine, and that’s why it creates users with sufficient privileges on Postgres. Then, generated credentials are automatically injected into auto-configured Spring Boot properties used for connecting with database spring.datasource.username and spring.datasource.password. The following picture illustrates the described solution.

testcontainers-1 (1).png

Ok, we know how it works, now the question is how to automatically test it. With Testcontainers it is possible with just a few lines of code.

1. Building application

Let’s begin from a short intro to the application code. It is very simple. Here’s the list of dependencies required for building an application that exposes REST API, and integrates with Postgres and Vault.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-vault-config-databases</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
   <groupId>org.postgresql</groupId>
   <artifactId>postgresql</artifactId>
   <version>42.2.5</version>
</dependency>

Application connects to Postgres, enables integration with Vault via Spring Cloud Vault, and automatically creates/updates tables on startup.

spring:
  application:
    name: callme-service
  cloud:
    vault:
      uri: http://192.168.99.100:8200
      token: ${VAULT_TOKEN}
      postgresql:
        enabled: true
        role: default
        backend: database
  datasource:
    url: jdbc:postgresql://192.168.99.100:5432/postgres
  jpa.hibernate.ddl-auto: update

It exposes the single endpoint. The following method is responsible for handling incoming requests. It just inserts a record to the database and returns a response with app name, version and id of inserted record.

@RestController
@RequestMapping("/callme")
public class CallmeController {

   private static final Logger LOGGER = LoggerFactory.getLogger(CallmeController.class);
   
   @Autowired
   Optional<BuildProperties> buildProperties;
   @Autowired
   CallmeRepository repository;
   
   @GetMapping("/message/{message}")
   public String ping(@PathVariable("message") String message) {
      Callme c = repository.save(new Callme(message, new Date()));
      if (buildProperties.isPresent()) {
         BuildProperties infoProperties = buildProperties.get();
         LOGGER.info("Ping: name={}, version={}", infoProperties.getName(), infoProperties.getVersion());
         return infoProperties.getName() + ":" + infoProperties.getVersion() + ":" + c.getId();
      } else {
         return "callme-service:"  + c.getId();
      }
   }
   
}

2. Enabling Testcontainers

To enable Testcontainers for our project we need to include some dependencies to our Maven pom.xml. We have dedicated modules for Postgres and Vault. We also include Spring Boot Test dependency, because we would like to test the whole Spring Boot app.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>vault</artifactId>
   <version>1.10.5</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>testcontainers</artifactId>
   <version>1.10.5</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>postgresql</artifactId>
   <version>1.10.5</version>
   <scope>test</scope>
</dependency>

3. Running Vault test container

Testcontainers framework supports JUnit 4/JUnit 5 and Spock. The Vault container can be started before tests if it is annotated with @Rule or @ClassRule. By default it uses version 0.7, but we can override it with the newest version, which is 1.0.2. We also may set a root token, which is then required by Spring Cloud Vault for integration with Vault.

@ClassRule
public static VaultContainer vaultContainer = new VaultContainer<>("vault:1.0.2")
   .withVaultToken("123456")
   .withVaultPort(8200);

That root token can be overridden before starting a JUnit test on the test class.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
    "spring.cloud.vault.token=123456"
})
public class CallmeTest { ... }

4. Running Postgres with Testcontainers

As an alternative to @ClassRule, we can manually start the container in a @BeforeClass or @Before method in the test. With this approach you will also have to stop it manually in @AfterClass or @After method. We start the Postgres container manually, because by default it is exposed on a dynamically generated port, which needs to be set for the Spring Boot application before starting the test. The listen port is returned by method getFirstMappedPort invoked on PostgreSQLContainer.

private static PostgreSQLContainer postgresContainer = new PostgreSQLContainer()
   .withDatabaseName("postgres")
   .withUsername("postgres")
   .withPassword("postgres123");
   
@BeforeClass
public static void init() throws IOException, InterruptedException {
   postgresContainer.start();
   int port = postgresContainer.getFirstMappedPort();
   System.setProperty("spring.datasource.url", String.format("jdbc:postgresql://192.168.99.100:%d/postgres", postgresContainer.getFirstMappedPort()));
   // ...
}

@AfterClass
public static void shutdown() {
   postgresContainer.stop();
}

5. Integrating Vault and Postgres containers

Once we have succesfully started both Vault and Postgres containers, we need to integrate them via Vault secret engine. First, we need to enable database secret engine Vault. After that we must configure a connection to Postgres. The last step is to configure a role. A role is a logical name that maps to a policy used to generate those credentials. All these actions may be performed using Vault commands. You can launch commands on the Vault container using execInContainer method. Vault configuration commands should be executed just after Postgres container startup.

@BeforeClass
public static void init() throws IOException, InterruptedException {
   postgresContainer.start();
   int port = postgresContainer.getFirstMappedPort();
   System.setProperty("spring.datasource.url", String.format("jdbc:postgresql://192.168.99.100:%d/postgres", postgresContainer.getFirstMappedPort()));
   vaultContainer.execInContainer("vault", "secrets", "enable", "database");
   String url = String.format("connection_url=postgresql://{{username}}:{{password}}@192.168.99.100:%d?sslmode=disable", port);
   vaultContainer.execInContainer("vault", "write", "database/config/postgres", "plugin_name=postgresql-database-plugin", "allowed_roles=default", url, "username=postgres", "password=postgres123");
   vaultContainer.execInContainer("vault", "write", "database/roles/default", "db_name=postgres",
      "creation_statements=CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";GRANT USAGE,  SELECT ON ALL SEQUENCES IN SCHEMA public TO \"{{name}}\";",
      "default_ttl=1h", "max_ttl=24h");
}

6. Running application tests

Finally, we may run application tests. We just call the single endpoint exposed by the app using TestRestTemplate, and verify the output.

@Autowired
TestRestTemplate template;

@Test
public void test() {
   String res = template.getForObject("/callme/message/{message}", String.class, "Test");
   Assert.assertNotNull(res);
   Assert.assertTrue(res.endsWith("1"));
}

If you are interested in what exactly happens during the test you can set a breakpoint inside the test method and execute docker ps command manually.

testcontainers-2

The post Testing Spring Boot Integration with Vault and Postgres using Testcontainers Framework appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2019/01/31/testing-spring-boot-integration-with-vault-and-postgres-using-testcontainers-framework/feed/ 0 6992
Integration tests on OpenShift using Arquillian Cube and Istio https://piotrminkowski.com/2018/09/13/integration-tests-on-openshift-using-arquillian-cube-and-istio/ https://piotrminkowski.com/2018/09/13/integration-tests-on-openshift-using-arquillian-cube-and-istio/#respond Thu, 13 Sep 2018 13:23:48 +0000 https://piotrminkowski.wordpress.com/?p=6798 In this tutorial I’ll show you how to use the Arquillian Cube OpenShift extension. Building integration tests for applications deployed on Kubernetes/OpenShift platforms seems to be quite a big challenge. With Arquillian Cube, an Arquillian extension for managing Docker containers, it is not complicated. Kubernetes extension, being a part of Arquillian Cube, helps you write […]

The post Integration tests on OpenShift using Arquillian Cube and Istio appeared first on Piotr's TechBlog.

]]>
In this tutorial I’ll show you how to use the Arquillian Cube OpenShift extension. Building integration tests for applications deployed on Kubernetes/OpenShift platforms seems to be quite a big challenge. With Arquillian Cube, an Arquillian extension for managing Docker containers, it is not complicated. Kubernetes extension, being a part of Arquillian Cube, helps you write and run integration tests for your Kubernetes/Openshift application. It is responsible for creating and managing temporary namespace for your tests, applying all Kubernetes resources required to setup your environment and once everything is ready it will just run defined integration tests.
The one very good information related to the Arquillian Cube OpenShift is that it supports the Istio framework. You can apply Istio resources before executing tests. One of the most important features of Istio is an ability to control traffic behavior with rich routing rules, retries, delays, failovers, and fault injection. It allows you to test some unexpected situations during network communication between microservices like server errors or timeouts.
If you would like to run some tests using Istio resources on Minishift you should first install it on your platform. To do that you need to change some privileges for your OpenShift user. Let’s do that.

1. Enabling Istio on Minishift

Istio requires some high-level privileges to be able to run on OpenShift. To add those privileges to the current user we need to login as an user with a cluster admin role. First, we should enable admin-user addon on Minishift by executing the following command.

$ minishift addons enable admin-user

After that you would be able to login as system:admin user, which has a cluster-admin role. With this user you can also add cluster-admin role to other users, for example admin. Let’s do that.

$ oc login -u system:admin
$ oc adm policy add-cluster-role-to-user cluster-admin admin
$ oc login -u admin -p admin

Now, let’s create a new project dedicated especially for Istio and then add some required privileges.

$ oc new-project istio-system
$ oc adm policy add-scc-to-user anyuid -z istio-ingress-service-account -n istio-system
$ oc adm policy add-scc-to-user anyuid -z default -n istio-system
$ oc adm policy add-scc-to-user anyuid -z prometheus -n istio-system
$ oc adm policy add-scc-to-user anyuid -z istio-egressgateway-service-account -n istio-system
$ oc adm policy add-scc-to-user anyuid -z istio-citadel-service-account -n istio-system
$ oc adm policy add-scc-to-user anyuid -z istio-ingressgateway-service-account -n istio-system
$ oc adm policy add-scc-to-user anyuid -z istio-cleanup-old-ca-service-account -n istio-system
$ oc adm policy add-scc-to-user anyuid -z istio-mixer-post-install-account -n istio-system
$ oc adm policy add-scc-to-user anyuid -z istio-mixer-service-account -n istio-system
$ oc adm policy add-scc-to-user anyuid -z istio-pilot-service-account -n istio-system
$ oc adm policy add-scc-to-user anyuid -z istio-sidecar-injector-service-account -n istio-system
$ oc adm policy add-scc-to-user anyuid -z istio-galley-service-account -n istio-system
$ oc adm policy add-scc-to-user privileged -z default -n myproject

Finally, we may proceed to Istio components installation. I downloaded the current newest version of Istio – 1.0.1. Installation file is available under install/kubernetes directory. You just have to apply it to your Minishift instance by calling oc apply command.

$ oc apply -f install/kubernetes/istio-demo.yaml

2. Enabling Istio for Arquillian Cube OpenShift

I have already described how to use Arquillian Cube to run tests with OpenShift in the article Testing microservices on OpenShift using Arquillian Cube. In comparison with the sample described in that article we need to include dependency responsible for enabling Istio features.

<dependency>
   <groupId>org.arquillian.cube</groupId>
   <artifactId>arquillian-cube-istio-kubernetes</artifactId>
   <version>1.17.1</version>
   <scope>test</scope>
</dependency>

Now, we can use @IstioResource annotation to apply Istio resources into OpenShift cluster or IstioAssistant bean to be able to use some additional methods for adding, removing resources programmatically or polling an availability of URLs.
Let’s take a look at the following JUnit test class using Arquillian Cube OpenShift with Istio support. In addition to the standard test created for running on the OpenShift instance I have added Istio resource file customer-to-account-route.yaml. Then I have invoked the method await provided by IstioAssistant. First test test1CustomerRoute creates new customer, so it needs to wait until customer-route is deployed on OpenShift. The next test test2AccountRoute adds an account for the newly created customer, so it needs to wait until account-route is deployed on OpenShift. Finally, the test test3GetCustomerWithAccounts is run, which calls the method responsible for finding a customer by id with a list of accounts. In that case customer-service calls method endpoint by account-service. As you have probably found out the last line of that test method contains an assertion to an empty list of accounts: Assert.assertTrue(c.getAccounts().isEmpty()). Why? We will simulate the timeout in communication between customer-service and account-service using Istio rules.

@Category(RequiresOpenshift.class)
@RequiresOpenshift
@Templates(templates = {
        @Template(url = "classpath:account-deployment.yaml"),
        @Template(url = "classpath:deployment.yaml")
})
@RunWith(ArquillianConditionalRunner.class)
@IstioResource("classpath:customer-to-account-route.yaml")
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class IstioRuleTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(IstioRuleTest.class);
    private static String id;

    @ArquillianResource
    private IstioAssistant istioAssistant;

    @RouteURL(value = "customer-route", path = "/customer")
    private URL customerUrl;
    @RouteURL(value = "account-route", path = "/account")
    private URL accountUrl;

    @Test
    public void test1CustomerRoute() {
        LOGGER.info("URL: {}", customerUrl);
        istioAssistant.await(customerUrl, r -> r.isSuccessful());
        LOGGER.info("URL ready. Proceeding to the test");
        OkHttpClient httpClient = new OkHttpClient();
        RequestBody body = RequestBody.create(MediaType.parse("application/json"), "{\"name\":\"John Smith\", \"age\":33}");
        Request request = new Request.Builder().url(customerUrl).post(body).build();
        try {
            Response response = httpClient.newCall(request).execute();
            ResponseBody b = response.body();
            String json = b.string();
            LOGGER.info("Test: response={}", json);
            Assert.assertNotNull(b);
            Assert.assertEquals(200, response.code());
            Customer c = Json.decodeValue(json, Customer.class);
            this.id = c.getId();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public  void test2AccountRoute() {
        LOGGER.info("Route URL: {}", accountUrl);
        istioAssistant.await(accountUrl, r -> r.isSuccessful());
        LOGGER.info("URL ready. Proceeding to the test");
        OkHttpClient httpClient = new OkHttpClient();
        RequestBody body = RequestBody.create(MediaType.parse("application/json"), "{\"number\":\"01234567890\", \"balance\":10000, \"customerId\":\"" + this.id + "\"}");
        Request request = new Request.Builder().url(accountUrl).post(body).build();
        try {
            Response response = httpClient.newCall(request).execute();
            ResponseBody b = response.body();
            String json = b.string();
            LOGGER.info("Test: response={}", json);
            Assert.assertNotNull(b);
            Assert.assertEquals(200, response.code());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test3GetCustomerWithAccounts() {
        String url = customerUrl + "/" + id;
        LOGGER.info("Calling URL: {}", customerUrl);
        OkHttpClient httpClient = new OkHttpClient();
        Request request = new Request.Builder().url(url).get().build();
        try {
            Response response = httpClient.newCall(request).execute();
            String json = response.body().string();
            LOGGER.info("Test: response={}", json);
            Assert.assertNotNull(response.body());
            Assert.assertEquals(200, response.code());
            Customer c = Json.decodeValue(json, Customer.class);
            Assert.assertTrue(c.getAccounts().isEmpty());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

3. Creating Istio rules

One of the interesting features provided by Istio is an availability of injecting faults to the route rules. we can specify one or more faults to inject while forwarding HTTP requests to the rule’s corresponding request destination. The faults can be either delays or aborts. We can define a percentage level of error using the percent field for both types of fault. In the following Istio resource I have defined a 2 seconds delay for every single request sent to account-service.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: account-service
spec:
  hosts:
    - account-service
  http:
  - fault:
      delay:
        fixedDelay: 2s
        percent: 100
    route:
    - destination:
        host: account-service
        subset: v1

Besides VirtualService we also need to define DestinationRule for account-service. It is really simple – we have just defined the version label of the target service.

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: account-service
spec:
  host: account-service
  subsets:
  - name: v1
    labels:
      version: v1

Before running the test we should also modify OpenShift deployment templates of our sample applications. We need to inject some Istio resources into the pods definition using istioctl kube-inject command as shown below.

$ istioctl kube-inject -f deployment.yaml -o customer-deployment-istio.yaml
$ istioctl kube-inject -f account-deployment.yaml -o account-deployment-istio.yaml

Finally, we may rewrite generated files into OpenShift templates. Here’s the fragment of the Openshift template containing DeploymentConfig definition for account-service.

kind: Template
apiVersion: v1
metadata:
  name: account-template
objects:
  - kind: DeploymentConfig
    apiVersion: v1
    metadata:
      name: account-service
      labels:
        app: account-service
        name: account-service
        version: v1
    spec:
      template:
        metadata:
          annotations:
            sidecar.istio.io/status: '{"version":"364ad47b562167c46c2d316a42629e370940f3c05a9b99ccfe04d9f2bf5af84d","initContainers":["istio-init"],"containers":["istio-proxy"],"volumes":["istio-envoy","istio-certs"],"imagePullSecrets":null}'
          name: account-service
          labels:
            app: account-service
            name: account-service
            version: v1
        spec:
          containers:
          - env:
            - name: DATABASE_NAME
              valueFrom:
                secretKeyRef:
                  key: database-name
                  name: mongodb
            - name: DATABASE_USER
              valueFrom:
                secretKeyRef:
                  key: database-user
                  name: mongodb
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  key: database-password
                  name: mongodb
            image: piomin/account-vertx-service
            name: account-vertx-service
            ports:
            - containerPort: 8095
            resources: {}
          - args:
            - proxy
            - sidecar
            - --configPath
            - /etc/istio/proxy
            - --binaryPath
            - /usr/local/bin/envoy
            - --serviceCluster
            - account-service
            - --drainDuration
            - 45s
            - --parentShutdownDuration
            - 1m0s
            - --discoveryAddress
            - istio-pilot.istio-system:15007
            - --discoveryRefreshDelay
            - 1s
            - --zipkinAddress
            - zipkin.istio-system:9411
            - --connectTimeout
            - 10s
            - --statsdUdpAddress
            - istio-statsd-prom-bridge.istio-system:9125
            - --proxyAdminPort
            - "15000"
            - --controlPlaneAuthPolicy
            - NONE
            env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: INSTANCE_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: ISTIO_META_POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: ISTIO_META_INTERCEPTION_MODE
              value: REDIRECT
            image: gcr.io/istio-release/proxyv2:1.0.1
            imagePullPolicy: IfNotPresent
            name: istio-proxy
            resources:
              requests:
                cpu: 10m
            securityContext:
              readOnlyRootFilesystem: true
              runAsUser: 1337
            volumeMounts:
            - mountPath: /etc/istio/proxy
              name: istio-envoy
            - mountPath: /etc/certs/
              name: istio-certs
              readOnly: true
          initContainers:
          - args:
            - -p
            - "15001"
            - -u
            - "1337"
            - -m
            - REDIRECT
            - -i
            - '*'
            - -x
            - ""
            - -b
            - 8095,
            - -d
            - ""
            image: gcr.io/istio-release/proxy_init:1.0.1
            imagePullPolicy: IfNotPresent
            name: istio-init
            resources: {}
            securityContext:
              capabilities:
                add:
                - NET_ADMIN
          volumes:
          - emptyDir:
              medium: Memory
            name: istio-envoy
          - name: istio-certs
            secret:
              optional: true
              secretName: istio.default

4. Building applications with Vert.x framework

The sample applications are implemented using Eclipse Vert.x framework. They use a Mongo database for storing data. The connection settings are injected into pods using Kubernetes Secrets.

public class MongoVerticle extends AbstractVerticle {

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

   @Override
   public void start() throws Exception {
      ConfigStoreOptions envStore = new ConfigStoreOptions()
            .setType("env")
            .setConfig(new JsonObject().put("keys", new JsonArray().add("DATABASE_USER").add("DATABASE_PASSWORD").add("DATABASE_NAME")));
      ConfigRetrieverOptions options = new ConfigRetrieverOptions().addStore(envStore);
      ConfigRetriever retriever = ConfigRetriever.create(vertx, options);
      retriever.getConfig(r -> {
         String user = r.result().getString("DATABASE_USER");
         String password = r.result().getString("DATABASE_PASSWORD");
         String db = r.result().getString("DATABASE_NAME");
         JsonObject config = new JsonObject();
         LOGGER.info("Connecting {} using {}/{}", db, user, password);
         config.put("connection_string", "mongodb://" + user + ":" + password + "@mongodb/" + db);
         final MongoClient client = MongoClient.createShared(vertx, config);
         final CustomerRepository service = new CustomerRepositoryImpl(client);
         ProxyHelper.registerService(CustomerRepository.class, vertx, service, "customer-service");   
      });
   }
}

MongoDB should be started on OpenShift before starting any applications, which connect to it. To achieve it we should insert a Mongo deployment resource into the Arquillian Cube configuration file as env.config.resource.name field.
The configuration of Arquillian Cube is visible below. We will use an existing namespace myproject, which has already granted the required privileges (see Step 1). We also need to pass the authentication token of user admin. You can collect it using command oc whoami -t after login to OpenShift cluster.

<extension qualifier="openshift">
   <property name="namespace.use.current">true</property>
   <property name="namespace.use.existing">myproject</property>
   <property name="kubernetes.master">https://192.168.99.100:8443</property>
   <property name="cube.auth.token">TYYccw6pfn7TXtH8bwhCyl2tppp5MBGq7UXenuZ0fZA</property>
   <property name="env.config.resource.name">mongo-deployment.yaml</property>
</extension>

The communication between customer-service and account-service is realized by Vert.x WebClient. We will set read timeout for the client to 1 second. Because Istio injects 2 seconds delay into the route, the communication is going to end with a timeout.

public class AccountClient {

   private static final Logger LOGGER = LoggerFactory.getLogger(AccountClient.class);
   private Vertx vertx;

   public AccountClient(Vertx vertx) {
      this.vertx = vertx;
   }
   
   public AccountClient findCustomerAccounts(String customerId, Handler>> resultHandler) {
      WebClient client = WebClient.create(vertx);
      client.get(8095, "account-service", "/account/customer/" + customerId).timeout(1000).send(res2 -> {
         if (res2.succeeded()) {
            LOGGER.info("Response: {}", res2.result().bodyAsString());
            List accounts = res2.result().bodyAsJsonArray().stream().map(it -> Json.decodeValue(it.toString(), Account.class)).collect(Collectors.toList());
            resultHandler.handle(Future.succeededFuture(accounts));
         } else {
            resultHandler.handle(Future.succeededFuture(new ArrayList<>()));
         }
      });
      return this;
   }
}

The full code of sample applications is available on GitHub in the repository https://github.com/piomin/sample-vertx-kubernetes/tree/openshift-istio-tests.

5. Running tests with Arquillian Cube OpenShift extension

You can run the tests during Maven build or just using your IDE. As the first test1CustomerRoute test is executed. It adds a new customer and save generated id for two next tests.

arquillian-istio-3

The next test is test2AccountRoute. It adds an account for the customer created during the previous test.

arquillian-istio-2

Finally, the test responsible for verifying communication between microservices is running. It verifies if the list of accounts is empty, what is a result of timeout in communication with account-service.

arquillian-istio-1

The post Integration tests on OpenShift using Arquillian Cube and Istio appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2018/09/13/integration-tests-on-openshift-using-arquillian-cube-and-istio/feed/ 0 6798
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
Continuous Integration with Jenkins, Artifactory and Spring Cloud Contract https://piotrminkowski.com/2018/07/04/continuous-integration-with-jenkins-artifactory-and-spring-cloud-contract/ https://piotrminkowski.com/2018/07/04/continuous-integration-with-jenkins-artifactory-and-spring-cloud-contract/#comments Wed, 04 Jul 2018 13:58:09 +0000 https://piotrminkowski.wordpress.com/?p=6709 Consumer Driven Contract (CDC) testing is one of the methods that allows you to verify integration between applications within your system. The number of such interactions may be really large especially if you maintain microservices-based architecture. Assuming that every microservice is developed by different teams or sometimes even different vendors, it is important to automate […]

The post Continuous Integration with Jenkins, Artifactory and Spring Cloud Contract appeared first on Piotr's TechBlog.

]]>
Consumer Driven Contract (CDC) testing is one of the methods that allows you to verify integration between applications within your system. The number of such interactions may be really large especially if you maintain microservices-based architecture. Assuming that every microservice is developed by different teams or sometimes even different vendors, it is important to automate the whole testing process. As usual, we can use Jenkins server for running contract tests within our Continuous Integration (CI) process.

The sample scenario has been visualized in the picture below. We have one application (person-service) that exposes API leveraged by three different applications. Each application is implemented by a different development team. Consequently, every application is stored in the separated Git repository and has a dedicated pipeline in Jenkins for building, testing and deploying.

contracts-3 (1)

The source code of sample applications is available on GitHub in the repository sample-spring-cloud-contract-ci (https://github.com/piomin/sample-spring-cloud-contract-ci.git). I placed all the sample microservices in the single Git repository only for our demo simplification. We will still treat them as separate microservices, developed and built independently.

In this article I used Spring Cloud Contract for CDC implementation. It is the first choice solution for JVM applications written in Spring Boot. Contracts can be defined using Groovy or YAML notation. After building on the producer side Spring Cloud Contract generates a special JAR file with stubs suffix, that contains all defined contracts and JSON mappings. Such a JAR file can be built on Jenkins and then published on Artifactory. Contract consumers also use the same Artifactory server, so they can use the latest version of stubs file. Because every application expects different response from person-service, we have to define three different contracts between person-service and a target consumer.

contracts-1

Let’s analyze the sample scenario. Assuming we have performed some changes in the API exposed by person-service and we have modified contracts on the producer side, we would like to publish them on a shared server. First, we need to verify contracts against producers (1), and in case of success publish an artifact with stubs to Artifactory (2). All the pipelines defined for applications that use this contract are able to trigger the build on a new version of JAR file with stubs (3). Then, the newest version contract is verifying against consumer (4). If contract testing fails, pipeline is able to notify the responsible team about this failure.

contracts-2

1. Pre-requirements

Before implementing and running any sample we need to prepare our environment. We need to launch Jenkins and Artifactory servers on the local machine. The most suitable way for this is through Docker containers. Here are the commands required for running these containers.

$ docker run --name artifactory -d -p 8081:8081 docker.bintray.io/jfrog/artifactory-oss:latest
$ docker run --name jenkins -d -p 8080:8080 -p 50000:50000 jenkins/jenkins:lts

I don’t know if you are familiar with such tools like Artifactory and Jenkins. But after starting them we need to configure some things. First you need to initialize Maven repositories for Artifactory. You will be prompted for that just after a first launch. It also automatically add one remote repository: JCenter Bintray (https://bintray.com/bintray/jcenter), which is enough for our build. Jenkins also comes with a default set of plugins, which you can install just after first launch (Install suggested plugins). For this demo, you will also have to install plugin for integration with Artifactory (https://wiki.jenkins.io/display/JENKINS/Artifactory+Plugin). If you need more details about Jenkins and Artifactory configuration you can refer to my older article How to setup Continuous Delivery environment.

2. Building contracts

We are beginning contract definition from the producer side application. Producer exposes only one GET /persons/{id} method that returns Person object. Here are the fields contained by Person class.

public class Person {

   private Integer id;
   private String firstName;
   private String lastName;
   @JsonFormat(pattern = "yyyy-MM-dd")
   private Date birthDate;
   private Gender gender;
   private Contact contact;
   private Address address;
   private String accountNo;

   // ...
}

The following picture illustrates, which fields of Person object are used by consumers. As you see, some of the fields are shared between consumers, while some others are required only by single consuming applications.

contracts-4

Now we can take a look at the contract definition between person-service and bank-service.

import org.springframework.cloud.contract.spec.Contract

Contract.make {
   request {
      method 'GET'
      urlPath('/persons/1')
   }
   response {
      status OK()
      body([
         id: 1,
         firstName: 'Piotr',
         lastName: 'Minkowski',
         gender: $(regex('(MALE|FEMALE)')),
         contact: ([
            email: $(regex(email())),
            phoneNo: $(regex('[0-9]{9}$'))
         ])
      ])
      headers {
         contentType(applicationJson())
      }
   }
}

For comparison, here’s the definition of contract between person-service and letter-service.

import org.springframework.cloud.contract.spec.Contract

Contract.make {
   request {
      method 'GET'
      urlPath('/persons/1')
   }
   response {
      status OK()
      body([
         id: 1,
         firstName: 'Piotr',
         lastName: 'Minkowski',
         address: ([
            city: $(regex(alphaNumeric())),
            country: $(regex(alphaNumeric())),
            postalCode: $(regex('[0-9]{2}-[0-9]{3}')),
            houseNo: $(regex(positiveInt())),
            street: $(regex(nonEmpty()))
         ])
      ])
      headers {
         contentType(applicationJson())
      }
   }
}

3. Implementing Spring Cloud Contract tests on the producer side

Ok, we have three different contracts assigned to the single endpoint exposed by person-service. We need to publish them in such a way that they are easily available for consumers. In that case Spring Cloud Contract comes with a handy solution. We may define contracts with different response for the same request, and then choose the appropriate definition on the consumer side. All those contract definitions will be published within the same JAR file. Because we have three consumers we define three different contracts placed in directories bank-consumer, contact-consumer and letter-consumer.

contracts-5

All the contracts will use a single base test class. To achieve it we need to provide a fully qualified name of that class for Spring Cloud Contract Verifier plugin in pom.xml.

<plugin>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-contract-maven-plugin</artifactId>
   <extensions>true</extensions>
   <configuration>
      <baseClassForTests>pl.piomin.services.person.BasePersonContractTest</baseClassForTests>
   </configuration>
</plugin>

Here’s the full definition of base class for our contract tests. We will mock the repository bean with the answer matching to the rules created inside contract files.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public abstract class BasePersonContractTest {

   @Autowired
   WebApplicationContext context;
   @MockBean
   PersonRepository repository;
   
   @Before
   public void setup() {
      RestAssuredMockMvc.webAppContextSetup(this.context);
      PersonBuilder builder = new PersonBuilder()
         .withId(1)
         .withFirstName("Piotr")
         .withLastName("Minkowski")
         .withBirthDate(new Date())
         .withAccountNo("1234567890")
         .withGender(Gender.MALE)
         .withPhoneNo("500070935")
         .withCity("Warsaw")
         .withCountry("Poland")
         .withHouseNo(200)
         .withStreet("Al. Jerozolimskie")
         .withEmail("piotr.minkowski@gmail.com")
         .withPostalCode("02-660");
      when(repository.findById(1)).thenReturn(builder.build());
   }
   
}

Spring Cloud Contract Maven plugin visible above is responsible for generating stubs from contract definitions. It is executed during Maven build after running mvn clean install command. The build is performed on Jenkins CI. Jenkins pipeline is responsible for updating remote Git repositories, building binaries from source code, running automated tests and finally publishing JAR files containing stubs on a remote artifact repository – Artifactory. Here’s Jenkins pipeline created for the contract producer side (person-service).

node {
  withMaven(maven:'M3') {
    stage('Checkout') {
      git url: 'https://github.com/piomin/sample-spring-cloud-contract-ci.git', credentialsId: 'piomin-github', branch: 'master'
    }
    stage('Publish') {
      def server = Artifactory.server 'artifactory'
      def rtMaven = Artifactory.newMavenBuild()
      rtMaven.tool = 'M3'
      rtMaven.resolver server: server, releaseRepo: 'libs-release', snapshotRepo: 'libs-snapshot'
      rtMaven.deployer server: server, releaseRepo: 'libs-release-local', snapshotRepo: 'libs-snapshot-local'
      rtMaven.deployer.artifactDeploymentPatterns.addInclude("*stubs*")
      def buildInfo = rtMaven.run pom: 'person-service/pom.xml', goals: 'clean install'
      rtMaven.deployer.deployArtifacts buildInfo
      server.publishBuildInfo buildInfo
    }
  }
}

We also need to include dependency spring-cloud-starter-contract-verifier to the producer app to enable Spring Cloud Contract Verifier.

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-contract-verifier</artifactId>
   <scope>test</scope>
</dependency>

4. Implementing Spring Cloud Contract tests on the consumer side

To enable Spring Cloud Contract on the consumer side we need to include artifact spring-cloud-starter-contract-stub-runner to the project dependencies.

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
   <scope>test</scope>
</dependency>

Then, the only thing left is to build a JUnit test, which verifies our contract by calling it through OpenFeign client. The configuration of that test is provided inside annotation @AutoConfigureStubRunner. We select the latest version of person-service stubs artifact by setting + in the version section of ids parameter. Because we have multiple contracts defined inside person-service we need to choose the right for current service by setting consumer-name parameter. All the contract definitions are downloaded from the Artifactory server, so we set stubsMode parameters to REMOTE. The address of the Artifactory server has to be set using repositoryRoot property.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"pl.piomin.services:person-service:+:stubs:8090"}, consumerName = "letter-consumer",  stubsPerConsumer = true, stubsMode = StubsMode.REMOTE, repositoryRoot = "http://192.168.99.100:8081/artifactory/libs-snapshot-local")
@DirtiesContext
public class PersonConsumerContractTest {

   @Autowired
   private PersonClient personClient;
   
   @Test
   public void verifyPerson() {
      Person p = personClient.findPersonById(1);
      Assert.assertNotNull(p);
      Assert.assertEquals(1, p.getId().intValue());
      Assert.assertNotNull(p.getFirstName());
      Assert.assertNotNull(p.getLastName());
      Assert.assertNotNull(p.getAddress());
      Assert.assertNotNull(p.getAddress().getCity());
      Assert.assertNotNull(p.getAddress().getCountry());
      Assert.assertNotNull(p.getAddress().getPostalCode());
      Assert.assertNotNull(p.getAddress().getStreet());
      Assert.assertNotEquals(0, p.getAddress().getHouseNo());
   }
   
}

Here’s Feign client implementation responsible for calling endpoint exposed by person-service

@FeignClient("person-service")
public interface PersonClient {

   @GetMapping("/persons/{id}")
   Person findPersonById(@PathVariable("id") Integer id);
   
}

5. Setup of Continuous Integration process

Ok, we have already defined all the contracts required for our exercise. We have also built a pipeline responsible for building and publishing stubs with contracts on the producer side (person-service). It always publishes the newest version of stubs generated from source code. Now, our goal is to launch pipelines defined for three consumer applications, each time when new stubs would be published to the Artifactory server by the producer pipeline.
The best solution for that would be to trigger a Jenkins build when you deploy an artifact. To achieve it we use Jenkins plugin called URLTrigger, that can be configured to watch for changes on a certain URL, in that case REST API endpoint exposed by Artifactory for selected repository path.
After installing URLTrigger plugin we have to enable it for all consumer pipelines. You can configure it to watch for changes in the returned JSON file from the Artifactory File List REST API, that is accessed via the following URI: http://192.168.99.100:8081/artifactory/api/storage/[PATH_TO_FOLDER_OR_REPO]/. The file maven-metadata.xml will change every time you deploy a new version of application to Artifactory. We can monitor the change of response’s content between the last two polls. The last field that has to be filled is Schedule. If you set it to * * * * * it will poll for a change every minute.

contracts-6

Our three pipelines for consumer applications are ready. The first run was finished with success.

contracts-7

If you have already built a person-service application and publish stubs to Artifactory you will see the following structure in libs-snapshot-local repository. I have deployed three different versions of API exposed by person-service. Each time I publish a new version of a contract all the dependent pipelines are triggered to verify it.

contracts-8

The JAR file with contracts is published under classifier stubs.

contracts-9

Spring Cloud Contract Stub Runner tries to find the latest version of contracts.

[code]2018-07-04 11:46:53.273 INFO 4185 — [ main] o.s.c.c.stubrunner.AetherStubDownloader : Desired version is [+] – will try to resolve the latest version
2018-07-04 11:46:54.752 INFO 4185 — [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved version is [1.3-SNAPSHOT]
2018-07-04 11:46:54.823 INFO 4185 — [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved artifact [pl.piomin.services:person-service:jar:stubs:1.3-SNAPSHOT] to /var/jenkins_home/.m2/repository/pl/piomin/services/person-service/1.3-SNAPSHOT/person-service-1.3-SNAPSHOT-stubs.jar

6. Testing changes in contract

Ok, we have already prepared contracts and configured our CI environment. Now, let’s perform change in the API exposed by person-service. We will just change the name of one field: accountNo to accountNumber.

contracts-12

This change requires a change in contract definition created on the producer side. If you modify the field name there person-service will build successfully and a new version of contract will be published to Artifactory. Because all other pipelines listen for changes in the latest version of JAR files with stubs, the build will be started automatically. Microservices letter-service and contact-service do not use field accountNo, so their pipelines will not fail. Only bank-service pipeline report error in contract as shown on the picture below.

contracts-10

Now, if you were notified about failed verification of the newest contract version between person-service and bank-service, you can perform required changes on the consumer side.

contracts-11

The post Continuous Integration with Jenkins, Artifactory and Spring Cloud Contract appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2018/07/04/continuous-integration-with-jenkins-artifactory-and-spring-cloud-contract/feed/ 2 6709