BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Improving Testability of Java Microservices with Container Orchestration and a Service Mesh

Improving Testability of Java Microservices with Container Orchestration and a Service Mesh

Bookmarks

Key Takeaways

  • In enterprise test scenarios, software needs to be tested in the same way as it will run in production, in order to ensure that it will work as expected. 
  • A common challenge is that microservice applications directly or indirectly depend on other services that need to be orchestrated within the test scenario.
  • This article shows how container orchestration provides an abstraction over service instances and facilitates in replacing them with mock instances.
  • Additionally, service meshes enable us to re-route traffic and inject faulty responses or delays to verify our services' resiliency.
  • The article contains sample code from an accompanying example Java-based coffee shop application deployed to and tested on Kubernetes and Istio.
Want to learn more about Service Mesh?
Read our ultimate guide to managing service-to-service communications in the era of microservices and cloud.
Read the guide
Service Mesh

In enterprise test scenarios, software needs to be tested in the same way as it will run in production, in order to ensure that it will work as expected. A common challenge is that microservice applications directly or indirectly depend on other services that need to be orchestrated within the test scenario.

This article shows how container orchestration provides an abstraction over service instances and facilitates in replacing them with mock instances. On top of that, service meshes enable us to re-route traffic and inject faulty responses or delays to verify our services' resiliency.

We will use a coffee shop example application that is deployed to a container orchestration and service mesh cluster. We have chosen Kubernetes and Istio as example environment technology.

Test Scenario

Let’s assume that we want to test the application’s behavior without considering other, external services. The application runs in the same way and is configured in the same way as in production, so that later on we can be sure that it will behave in exactly the same way. Our test cases will connect to the application by using its well-defined communication interfaces.

External services, however, should not be part of the test scenario. In general, test cases should focus on a single object-under-test and mask out all the rest. Therefore, we substitute the external services with mock servers.

 

 

Container Orchestration

Reconfiguring the application to use the mock servers instead of the actual backends contradicts the idea of running the microservice in the same way as in production, since this would chance configuration. However, if our application is deployed to a container orchestration cluster, such as Kubernetes, we can use the abstracted service names as configured destinations and let the cluster resolve the backend service instances.

The following example shows a gateway class that is part of the coffee shop application and connects against the coffee-processor host on port 8080.

 

public class OrderProcessor {

    // definitions omitted ...

    @PostConstruct
    private void initClient() {
        final Client client = ClientBuilder.newClient();
        target = client.target("http://coffee-processor:8080/processes");
    }

   @Transactional(Transactional.TxType.REQUIRES_NEW)
    public void processOrder(Order order) {
        OrderStatus status = retrieveOrderStatus(order);
        order.setStatus(status);
        entityManager.merge(order);
    }

    // ...

    private JsonObject sendRequest(final JsonObject requestBody) {
        Response response = target.request()
               .buildPost(Entity.json(requestBody))
                .invoke();

        // ...

        return response.readEntity(JsonObject.class);
    }

    // definitions omitted ...
}

This host name is resolved via the Kubernetes cluster DNS and this will direct traffic to one of the running processor instances. The instance that backs the coffee-processor service, however, will be a mock server, WireMock in our example. This substitution is transparent to our application.

The system test scenario not only connects against the application to invoke the desired business use case, but will also communicate with the mock server, on a separate admin interface, to control its response behavior and to verify whether the application invoked the mock in the correct way. It is the same idea as for class-level unit tests, usually realized by JUnit and Mockito.

 

 

External Services

This setup allows us to mock and control services that run inside of our container orchestration cluster. But what if the external service is outside of the cluster?

In general, we can create a Kubernetes service without selectors that points to an external IP, and rewrite our application to always use that service name which is resolved by the cluster. By doing so, we define a single point of responsibility, where the service will route to.

The following code snippet shows an external Kubernetes service and endpoints definition which routes coffee-shop-db to an external IP address 1.2.3.4:
 

kind: Service
apiVersion: v1
metadata:
  name: coffee-shop-db
spec:
  ports:
  - protocol: TCP
    port: 5432
---

kind: Endpoints
apiVersion: v1
metadata:
  name: coffee-shop-db
subsets:
  - addresses:
      - ip: 1.2.3.4
    ports:
      - port: 5432


Within different environments, the service might route to different database instances.

Service Meshes

Service meshes enable us to transparently support technical cross-cutting communication concerns to microservices. As of today, Istio is one of the most-used service mesh technology. It adds sidecar proxy containers, co-located with our application containers, which implement these additional concerns. The proxy containers also allow to purposely manipulate or slow down connection for resiliency testing purposes.

In a end-to-end test, we can introduce faulty or slow responses to verify whether our applications handles these faulty situations properly.

The following code snippet shows an Istio virtual service definition that annotates the route to coffee-processor with a 3 second delay for 50% and failures for 10% of the responses.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: coffee-processor
spec:
  hosts:
  - coffee-processor
  http:
  - route:
    - destination:
        host: coffee-processor
        subset: v1
    fault:
      delay:
        fixedDelay: 3s
        percent: 50
      abort:
        httpStatus: 500
        percent: 10
---

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

Now, we can run additional tests and verify how our application reacts to these increased response times and failure situations.

Besides the possibility to inject faulty responses, service mesh technology also allows to add resiliency from the environment. Proxy containers can handle timeouts, implement circuit breakers and bulkheads without requiring the application to handle these concerns.

Conclusion

Container orchestration and service meshes improve the testability of microservice applications by extracting concerns from the application into the operational environment. Service abstractions implement discovery and allow us to transparently substitute services or to re-route. Service meshes not only allow more complex routing but also allow us to inject failures or slow responses in order to put our applications under pressure and verify their corresponding behavior.

Further resources

About the Author

 Sebastian Daschner is a self-employed Java consultant, author and trainer and is enthusiastic about programming and Java. He is the author of the book ‘Architecting Modern Java EE Applications’. Sebastian is participating in the JCP, helping forming the future standards of Java EE, serving in the JAX-RS, JSON-P and Config Expert Groups and collaborating on various open source projects. For his contributions in the Java community and ecosystem he was recognized as a Java Champion, Oracle Developer Champion and double 2016 JavaOne Rockstar. Besides Java, Sebastian is also a heavy user of Linux and cloud native technologies. He evangelizes computer science practices on his newsletter, and on Twitter via @DaschnerS. When not working with Java, he also loves to travel the world — either by plane or motorbike.

Rate this Article

Adoption
Style

BT