Microservice architecture makes it possible for teams to develop services at their own speed. However, any company that adopts a microservice architecture will face challenges in running and evolving their services if they don’t implement a solution to mitigate it.
One of the major problems that companies face when they switch to a microservice architecture is maintaining a clean API for each microservice in their application. Because each team can change the service they’re responsible for at any time, there’s an increased risk of introducing a breaking change in the API and tanking the entire system.
Making sure each change in the service is backward compatible and not breaking the API contract is of the utmost importance to keep the system running smoothly. In this article, we’ll look at how to implement contract testing in a microservices architecture to avoid releasing buggy services in production.
Imagine an eCommerce platform built with a microservice architecture, like the one shown below.
Figure 1: Sample Microservice architecture for an eCommerce platform
In this diagram, you can see there are four microservices responsible for fulfilling the order workflow. Naturally, there are many more services in an eCommerce platform, but for simplicity’s sake, we’re only going to look at the order workflow.
The process goes like this:
- The Order service sends a request to the inventory service to find out if there is sufficient inventory to fill an order.
- If the inventory is sufficient, the order service sends a request to the payments service to process the payment for the order.
- Once the payment is processed, the payment service sends a request to the shipping service and adds the order to the shipping queue.
Pretty straightforward, right? You might be wondering what could go wrong. Let’s look into how breaking the API contract can affect the entire order workflow.
- If the inventory service changed its API contract without the order service knowing, the order service couldn’t confirm if there was enough inventory to process the order. The user would then receive an error that “Orders cannot be processed at this time,” causing frustration and possibly leading the user to take their business elsewhere.
- If the shipping service changes its API contract without the payments service’s knowledge, the payments service won’t be unable to place a shipping request. Even after a successful payment, the user won’t receive an Order confirmation, again causing user frustration.
There will be similar problems if any of the microservices change their API contract without informing the dependent microservices about the change. This directly impacts user experience and, ultimately, revenue.
In all of these scenarios, each service was thoroughly tested for its own functionality, but the issues came about because there were no tests to see if individual changes would affect any other service in the system. That’s where contract testing can be helpful. Contract tests help you flag errors early in the system before a broken API contract makes it to production.
So, how do you set up contract tests?
For every change made in any service, run a set of tests against the latest versions of all other services. This will tell you if everything is still working. In order to keep your tests lean, only run these tests for dependent services, rather than the entire system.
Figure 2: Sample contract testing architecture
Let’s look back to our eCommerce order workflow for an example. If a change is made in the inventory service, we can run our tests on the latest version of each service inside a Docker container in our test environment. If the inventory service has made any breaking contract changes, our tests will fail and the build will be halted (as shown in figure 2). This lets us stop the deployment of a buggy service in the production environment so we can maintain a good customer experience.
Now that we’ve solved the issue of breaking contracts with contract tests, we have a new problem.
With hundreds of microservices and thousands of associated contract tests, it’s difficult to track down a single failed build. Tracking down which tests failed and why becomes its own daunting task. With a lack of visibility into tests running in the CI pipeline, and no way to debug them, finding the issue will be time-consuming and lead to longer build times and release cycles.
Debugging Failing Tests
One of the ways developers can debug their failing contract tests is by setting up the entire system of microservices on their local environment and debugging the tests using their integrated development environment (IDE). This approach can work if your enterprise only has a few developers, but as the number of developers grows, maintaining automated scripts to set up all the services correctly on the local system can pose its own issue.
In such cases, a tool like Thundra Foresight can alleviate the pain of debugging tests by giving you full visibility into your testing pipelines. Foresight provides a summary for each individual test and gives the historical test statuses and runtimes, making it easy to find slow and flaky tests.
You’ll receive contextualized information, such as logs, metrics, and traces for every test, helping you identify the root cause of failures. Thundra Foresight also gives you detailed distributed tracing maps, making failure information easy to access and errors quick to fix.
With Foresight, you can do time-travel debugging. This means stepping into each line of code and checking variables at each step, recreating the failing test scenario to get a better understanding of the system.
Having a microservice architecture allows you to move quickly and independently, but evolving and maintaining API contracts introduces a layer of complexity. You can alleviate the issue by implementing contract tests, but you may find yourself with longer build cycles as you struggle to debug failing tests.
But it doesn’t have to be that way. With contextual summaries, distributed tracing, and time travel debugging, Foresight keeps your builds green by optimizing your tests and the whole CI pipeline. Take the pain out of debugging tests with Thundra Foresight.