Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Testing Microservices on OpenShift Using Arquillian Cube

DZone's Guide to

Testing Microservices on OpenShift Using Arquillian Cube

Looking for a new integration solution? Arquillian Cube is a new option for integration testing instead of using Kubernetes and OpenShift platforms.

· Microservices Zone ·
Free Resource

Learn the Benefits and Principles of Microservices Architecture for the Enterprise

When I had an encounter with the Arquillian framework for the first time, I was building automated end-to-end tests for JavaEE based applications. At that time, testing applications deployed on JavaEE servers were not very comfortable. Arquillian was a nice solution to that problem. Arquillian provides useful mechanisms for testing EJBs deployed on an embedded application server.

Currently, Arquillian provides multiple modules dedicated to different technologies and use cases. One of these modules is Arquillian Cube. With this extension, you can create integration/ functional tests running on Docker containers or even more advanced orchestration platforms, like Kubernetes or OpenShift.

In this article, I'm going to show you how to use Arquillian Cube for building integration tests for applications running on OpenShift platform. All these examples will be deployed locally on Minishift. Here's the full list of topics covered in this article:

  • Using Arquillian Cube for deploying and running applications on Minishift
  • Testing applications deployed on Minishift by calling their REST API exposed using OpenShift routes
  • Testing inter-service communication between deployed applications, based on Kubernetes services

Before reading this article, it is worth considering two of my previous articles about Kubernetes and OpenShift:

The following picture illustrates the architecture of the currently discussed solution. We will build and deploy two sample applications on Minishift. They integrate with NoSQL database, which is also run as a service on OpenShift platform.

Now, we may proceed to the development.

1. Including Arquillian Cube Dependencies

Before including dependencies to Arquillian Cube libraries, we should define the dependency management section in our pom.xml. It should contain BOM of Arquillian framework and its Cube extension.

<dependencyManagement>
     <dependencies>
          <dependency>
                <groupId>org.arquillian.cube</groupId>
                <artifactId>arquillian-cube-bom</artifactId>
                <version>1.15.3</version>
                <scope>import</scope>
                <type>pom</type>
          </dependency>
          <dependency>
                <groupId>org.jboss.arquillian</groupId>
                <artifactId>arquillian-bom</artifactId>
                <version>1.4.0.Final</version>
                <scope>import</scope>
                <type>pom</type>
          </dependency>
     </dependencies>
</dependencyManagement>

Here's the list of libraries used in my sample project. The most important thing is to include starter for Arquillian Cube OpenShift extension, which contains all required dependencies. It is also important to include the arquillian-cube-requirement artifact, if you would like to annotate a test class with @RunWith(ArquillianConditionalRunner.class) and openshift-client , in case you would like to use Fabric8 OpenShiftClient.

<dependency>
     <groupId>org.jboss.arquillian.junit</groupId>
     <artifactId>arquillian-junit-container</artifactId>
     <version>1.4.0.Final</version>
     <scope>test</scope>
</dependency>
<dependency>
     <groupId>org.arquillian.cube</groupId>
     <artifactId>arquillian-cube-requirement</artifactId>
     <scope>test</scope>
</dependency>
<dependency>
     <groupId>org.arquillian.cube</groupId>
     <artifactId>arquillian-cube-openshift-starter</artifactId>
     <scope>test</scope>
</dependency>
<dependency>
     <groupId>io.fabric8</groupId>
     <artifactId>openshift-client</artifactId>
     <version>3.1.12</version>
     <scope>test</scope>
</dependency>

2. Running Minishift

In my previous articles about OpenShift, I provided detailed instructions on how to run Minishift locally. Here's the full list of commands that should be executed in order to start Minishift, reuse Docker daemon managed by Minishift, and create test namespace (project).

$ minishift start --vm-driver=virtualbox --memory=2G
$ minishift docker-env
$ minishift oc-env
$ oc login -u developer -p developer
$ oc new-project sample-deployment

We also have to create a Mongo database service on OpenShift. OpenShift is a platform that provides an easy way to deploy built-in services via a web console available at https://192.168.99.100:8443. You can select the required service on the main dashboard and confirm the installation using default properties. Otherwise, you would have to provide a YAML template with deployment configuration and apply it to Minishift using the oc command. The YAML file will also be required, if you decide to recreate namespace on every single test case (explained in the subsequent text in Step 3). If you would like to view the template with configuration for creating MongoDB service on Minishift, this file is available in my GitHub repository in the /openshift/mongo-deployment.yaml file. To access that file you need to clone repository sample-vertx-kubernetes and switch to branch openshift ( https://github.com/piomin/sample-vertx-kubernetes/tree/openshift-tests). It contains definitions of secret, persistentVolumeClaim, deploymentConfig and service.

3. Configuring Connection With Minishift for Arquillian

All the Arquillian configuration settings should be provided in a arquillian.xml file located in thesrc/test/resources directory. When running Arquillian tests on Minishift, you generally have two approaches that may be applied. You can create new namespace per every test suite and then remove it after the test or you can use the existing one. Then, remove all the created components within the selected namespace. First, the approach is set by default for every test, until you modify it inside your Arquillian configuration file, using namespace.use.existing and namespace.use.current properties.

<extension qualifier="openshift">
<property name="namespace.use.current">true</property>
<property name="namespace.use.existing">sample-deployment</property>
<property name="kubernetes.master">https://192.168.99.100:8443</property>
<property name="cube.auth.token">EMNHP8QIB4A_VU4kE_vQv8k9he_4AV3GTltrzd06yMU</property>
</extension>

You will also need to set the Kubernetes master address and API token. In order to obtain the API token, run the following command:

$ oc whoami -t
EMNHP8QIB4A_VU4kE_vQv8k9he_4AV3GTltrzd06yMU

4. Building Arquillian JUnit Test

Every JUnit test class should be annotated with @RequiresOpenshift. It should also have a runner set. In this case, it will be ArquillianConditionalRunner. The test method testCustomerRoute applies to the configuration passed inside file deployment.yaml, which is assigned to the method using the @Template annotation.
The important part of this unit test is the route's URL declaration. We have to annotate it with the following annotation:

  • @RouteURL — this searches for a route with a name defined using value parameter and inject it into URL object instance
  • @AwaitRoute — if you do not declare this annotation, the test will finish just after running, because deployment on OpenShift is processed asynchronously. @AwaitRoute will force the test to wait until a route is available on Minishift. We can set the timeout of waiting for the route (in this case it is 2 minutes) and the route's path. Establishing the route's path is very important here. Without it, our test won't locate the route and will finish with a 2-minute timeout.

The test method is very simple. In fact, I only send a POST request with a JSON object to the endpoint assigned to the customer-route route and verify if the HTTP status code is 200. Because I had a problem with injecting the route's URL. In fact, it doesn't work for my sample with Minishift v3.9.0, while it works with Minishift v3.7.1. Next, I needed to prepare it manually in the code. If it works properly, we could use the URL url instance for that.

@Category(RequiresOpenshift.class)
@RequiresOpenshift
@RunWith(ArquillianConditionalRunner.class)
public class CustomerServiceApiTest {

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

    @ArquillianResource
    OpenShiftAssistant assistant;
    @ArquillianResource
    OpenShiftClient client;

    @RouteURL(value = "customer-route")
    @AwaitRoute(timeoutUnit = TimeUnit.MINUTES, timeout = 2, path = "/customer")
    private URL url;

    @Test
    @Template(url = "classpath:deployment.yaml")
    public void testCustomerRoute() {
        OkHttpClient httpClient = new OkHttpClient();
        RequestBody body = RequestBody.create(MediaType.parse("application/json"), "{\"name\":\"John Smith\", \"age\":33}");
        Request request = new Request.Builder().url("http://customer-route-sample-deployment.192.168.99.100.nip.io/customer").post(body).build();
        try {
            Response response = httpClient.newCall(request).execute();
            LOGGER.info("Test: response={}", response.body().string());
            Assert.assertNotNull(response.body());
            Assert.assertEquals(200, response.code());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5. Preparing Deployment Configuration

Before running the test, we have to prepare a template configuration, which is loaded by Arquillian Cube, using the @Template annotation. Next, we need to create the deploymentConfig, inject the MongoDB credentials stored in the secret object, and, finally, expose the service outside container using the route object.

kind: Template
apiVersion: v1
metadata:
  name: customer-template
objects:
  - kind: ImageStream
    apiVersion: v1
    metadata:
      name: customer-image
    spec:
      dockerImageRepository: piomin/customer-vertx-service
  - kind: DeploymentConfig
    apiVersion: v1
    metadata:
      name: customer-service
    spec:
      template:
        metadata:
          labels:
            name: customer-service
        spec:
          containers:
          - name: customer-vertx-service
            image: piomin/customer-vertx-service
            ports:
            - containerPort: 8090
              protocol: TCP
            env:
            - name: DATABASE_USER
              valueFrom:
                secretKeyRef:
                  key: database-user
                  name: mongodb
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  key: database-password
                  name: mongodb
            - name: DATABASE_NAME
              valueFrom:
                secretKeyRef:
                  key: database-name
                  name: mongodb
      replicas: 1
      triggers:
      - type: ConfigChange
      - type: ImageChange
        imageChangeParams:
          automatic: true
          containerNames:
          - customer-vertx-service
          from:
            kind: ImageStreamTag
            name: customer-image:latest
      strategy:
        type: Rolling
      paused: false
      revisionHistoryLimit: 2
      minReadySeconds: 0
  - kind: Service
    apiVersion: v1
    metadata:
      name: customer-service
    spec:
      ports:
      - name: "web"
        port: 8090
        targetPort: 8090
      selector:
        name: customer-service
  - kind: Route
    apiVersion: v1
    metadata:
      name: customer-route
    spec:
      path: "/customer"
      to:
        kind: Service
        name: customer-service

6. Testing Inter-Service Communication

In the sample project, the communication with other microservices is realized by Vert.x WebClient. It reads the Kubernetes service name and its container port as parameters. It is implemented inside customer-service by AccountClient, which is then invoked inside the Vert.x HTTP route implementation. Here's the AccountClient implementation:

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 < AsyncResult < List >> resultHandler) {
        WebClient client = WebClient.create(vertx);
        client.get(8095, "account-service", "/account/customer/" + customerId).send(res2 - > {
            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));
        });
        return this;
    }
}

The endpoint GET /account/customer/:customerId is exposed by account-service and is called within the implementation of the method GET /customer/:id and exposed by customer-service. This time, we will create a new namespace, instead of using the existing one. That's why we have to apply MongoDB deployment configuration before applying the configuration of sample services. We also need to upload configuration of account-service that is provided inside the account-deployment.yaml file. The remaining part of the JUnit test is pretty similar to the test described in Step 4. It waits until the customer-route is available on Minishift. The only differences are when calling a URL and dynamic injection of namespace into the route's URL.

@Category(RequiresOpenshift.class)
@RequiresOpenshift
@RunWith(ArquillianConditionalRunner.class)
@Templates(templates = {
        @Template(url = "classpath:mongo-deployment.yaml"),
        @Template(url = "classpath:deployment.yaml"),
        @Template(url = "classpath:account-deployment.yaml")
})
public class CustomerCommunicationTest {

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

    @ArquillianResource
    OpenShiftAssistant assistant;

    String id;

    @RouteURL(value = "customer-route")
    @AwaitRoute(timeoutUnit = TimeUnit.MINUTES, timeout = 2, path = "/customer")
    private URL url;

    // ...

    @Test
    public void testGetCustomerWithAccounts() {
        LOGGER.info("Route URL: {}", url);
        String projectName = assistant.getCurrentProjectName();
        OkHttpClient httpClient = new OkHttpClient();
        Request request = new Request.Builder().url("http://customer-route-" + projectName + ".192.168.99.100.nip.io/customer/" + id).get().build();
        try {
            Response response = httpClient.newCall(request).execute();
            LOGGER.info("Test: response={}", response.body().string());
            Assert.assertNotNull(response.body());
            Assert.assertEquals(200, response.code());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Then, you can run the test using your IDE or simply by executing the command mvn clean install.

Conclusion

Arquillian Cube comes with a gentle solution for integration testing over Kubernetes and OpenShift platforms. It is not difficult to prepare and upload the configuration with database and microservices and then deploying it on the OpenShift node. You can event test the communication between microservices just by deploying a dependent application with the OpenShift template.

Microservices for the Enterprise eBook: Get Your Copy Here

Topics:
microservices ,testing ,arquillian ,tutorial ,minishift ,docker ,arquillian cube

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}