The ability to break applications into discrete, nimble, modular parts is what makes microservices appealing. However, this characteristic creates some challenges -- particularly when it comes to testing.
The distributed nature of microservices, combined with the sheer number that live within an application, make it much harder for developers to perform the integration tests that were a straightforward, routine part of monolithic app development.
For that reason, developers who work with microservices might want to adopt a practice known as contract testing. Contract testing offers a simpler, more manageable way to ensure that microservices perform as required. Let's explore how microservices contract testing works, the benefits it offers compared to classic integration testing and how to implement a strong contract testing strategy.
The problem with microservices integration testing
End-to-end integration tests evaluate how multiple services interact by spinning up replica instances of those individual services and running tests against each one. It's certainly possible to test a microservice-based application using end-to-end integration tests, and it's often an adequate approach for a relatively simple application that only encompasses a handful of microservices.
When dealing with large, complex collections of microservices, however, end-to-end integration tests become hard to manage. The major reasons for this include:
- Too many integrations to test. There are simply too many integration paths that exist within large collections of microservices -- and too many complex dependencies -- to ever be sure each possible interaction was tested.
- Mismatched microservice versions. Because microservices are developed independently, sometimes using separate CI/CD pipelines, it becomes difficult to keep track of each microservice's individual version stage. By the time an environment is set up to run integration tests, some of the microservice versions simulated in that environment might already be out of date.
- Lack of clear testing requirements. The amount of functionality a developer could test for in a full application is virtually unlimited. However, one person can write only a limited number of tests. When each microservice embodies its own unique set of functionalities, it can be extremely hard to define a reasonable scope for end-to-end integration tests. This increases the risk that the tests team members write fail to cover important functionality.
What is contract testing?
Contract testing is an approach where developers first determine how the individual services within one application system should interact with each other. With this information, teams create virtual contracts that define how exactly two microservices should interact. This contract will provide the benchmark when testing future microservices interactions.
When you perform a contract test, one microservice is categorized as the provider, meaning it is responsible for serving certain types of information in certain formats. The other microservice is the consumer, which is responsible for requesting information from the provider in a certain way. Typically, microservice contract tests start with a mock of the provider microservice, which will issue requests to the actual consumer microservice.
This approach allows you to run tests without the need to stand up full, live implementations of each microservice. On top of that, it's possible to effectively test each microservice independently of the others. This is one of several benefits that contract testing offers over traditional integration testing.
The steps for running contract tests are straightforward:
- Determine which microservices to test.
- Establish a contract that defines factors that govern the way the microservices should interact, such as the request parameters and expected response body.
- Deploy an instance of the consumer microservice and a mock of the provider.
- Issue requests from the consumer to the provider microservice.
- Evaluate the result of that request against the contract requirements.
Most of these processes outlined above can be scripted and automated. The only step that typically requires significant manual effort is determining the contents of the contract. However, there are some tools out there, such as Pact, that can automatically generate these contracts. Mocking tools such as Microcks and Mockintosh can also come in handy during contract tests.
The benefits of microservice contract testing
Contract-based application design and development is hardly an avant-garde concept; in fact, its documented history dates back at least 30 years. However, while this concept wasn't necessarily tailor-made for microservices, its basic premise suits itself to the modern challenges of distributed service management.
If you build microservice-based applications, contract testing offers simple and efficient means of testing those applications at scale. The orchestration effort for contract testing is, in some respects, higher than your average integration test, and contract tests require significant coordination between those working on various pieces of the application. Ultimately, contract testing can often yield more reliable results than traditional, end-to-end integration testing on microservices.
A specific benefit is that contract tests focus only on two microservices at a same time, which can break up testing routines into much more manageable segments. The use of mocks instead of full-fledged service builds simplifies the process even more because it eliminates the need to worry about back-end provisioning.
A contract-based approach also lets you limit testing scope to just the functionality of the two microservices involved. This results in simplified tests that can systematically evaluate how each pair of microservices interacts. Because there are only two microservices tested at a time, it becomes much easier to keep track of the microservice versions involved.
Limitations of contract tests
Contract testing for microservices is not without its challenges. Chief among them is that you'll need to run many individual tests to evaluate all microservices within your application. In this sense, contract testing is trickier to orchestrate than end-to-end integration testing. Integration testing only requires one testing environment and one set of tests. Contract testing requires teams to run a long series of tests, each for a different pair of microservices.
Another major limitation of contract testing revolves around the use of mocks. Although a mock is a reliable way of mimicking microservice behavior without running a microservice, there's no guarantee that the actual microservice will behave in the same way when in production. Due to the nature of the instances spun up, integration testing isn't subject to this same limitation.
Finally, teams must be sure to keep contracts up to date. The contract testing process needs to loop in each development team and its members so that they can help to define new contract requirements whenever they make changes to a microservice's functionality or behavior. Otherwise, tests will become based on contracts that no longer accurately reflect the desired behavior of a microservice.