The application stack has undergone fundamental changes with the advent of microservices architecture, and this has had a ripple effect on software testing. For a monolithic application released every quarter, testing was reserved for the very last week or two before release. Today, with micro-releases happening multiple times daily, software testing is more granular, it happens simultaneously with development, and it is fundamentally different from testing for a monolithic application.
1. UNIT TESTS AND MICROSERVICES — LIKE PB&J
Unit tests are always an important part of a QA strategy, but they are more so with microservices. The microservices architecture decomposes the monolithic application into smaller interdependent services. Each service runs a single feature, or at least that’s the goal—although when transitioning a monolith to microservices initially, it’s normal to have multiple features included in a single service. Assuming a single service runs just one feature, unit tests fit perfectly into this model because they require the testing of the most basic functionality of pieces of code. Unit tests operate at the level of the smallest component of the application.
Unit tests help keep the scope of tests limited to just one area of functionality. This way, each unit test has a single goal that’s clearly defined. While in a monolith it can be hard to determine the root cause for a failed test, with unit tests run on microservices, it becomes much easier to identify the failure.
Avoiding false positives helps improve the quality of testing, and this is done by combining microservices with unit testing. Limiting the scope of tests also makes tests run faster. With the dual benefits of focus and speed, unit tests are indispensable to microservices.
2. TESTING INTEGRATION BETWEEN SERVICES
Going a level higher than unit tests, we come to integration tests, which still have a place in microservices. Integration tests are used to check how each service works with other services and with external components. They are not concerned with the behavior of each service internally, but focus instead on the communication between services. They can also be used to test external components like databases.
Integration tests should come after there is adequate coverage with unit tests. They should be run especially when new features are introduced, as they ensure the new features are compatible with other parts of the application.
Another important aspect of service-to-service communication is tracing. Typically, any request would touch multiple services before it circles back to the user with a response. In this scenario, it’s important to have observability and monitoring of requests across services. Tracing is a great way to achieve this. New open source tools like Jaeger help to break up a single request into an easy-to-view visual that shows how many services it touches, and the duration for each service. This is very useful to troubleshoot latencies and identify bottlenecks.
3. PLAN TO FAIL SMALL
Post-deployment, it’s important to ensure the reliability of the application. Microservices architecture already helps with this by isolating services from one another so that even if one service fails, it doesn’t take down the neighboring services. Going a step further, you can build in resiliency by practicing chaos engineering — a concept made popular by Netflix when they announced their Chaos Monkey tool. This tool would randomly kill instances and services and force engineers to respond and ensure there’s little or no downtime. Failures are inevitable, and chaos engineering helps you to be prepared for failures at any time.
However, you can’t just start right away. You’ll need to build up to a full-blown chaos engineering practice by starting small. Initially, you could fail services and instances manually,and then graduate to inducing failures in a random, automated fashion.
To implement this, you could use a standalone tool like Chaos Monkey. You could also use a microservice networking tool like Istio. Istio can automatically route traffic in a way that induces faults and delays into HTTP requests. The HTTPFaultInjection feature of Istio enables you to delay or abort a request intentionally.
In a Kubernetes-centric world, it’s important to go beyond exclusive testing tools and explore modern cloud-native tools like Jaeger and Istio.
4. TESTING AS PART OF GITOPS
Though continuous integration has been around for a while, today, much of the innovation is around continuous deployment—specifically with GitOps (a way of automating deployments starting from a GitHub repository). GitOps introduces the automation and speed that DevOps has always been about, but is only now being realized with the help of cloud-native tools like Jenkins X, Helm, and Grafeas.
While GitOps automates every step after the pull request, it’s easy to compromise on testing. However, for the GitOps model to be successful, test automation is required so that every deployment is tested and approved before a “merge.” Without this, there’s no way to maintain the quality of what’s being deployed. Tools like Helm and Jenkins X accelerate the software delivery pipeline, but they call for a keen focus on test automation.
In conclusion, microservices brings fundamental change to application architecture by decomposing a monolith into numerous microservices. These services need to be further broken down and tested with unit tests. Once adequate unit tests are in place, integration tests should check the communication between services. Post-deployment, chaos engineering can help build more resilient applications. Additionally, test automation is key to enabling successful GitOps. As you put these focus areas for testing in place, you’ll be at home in the new world of microservices.