Scaling is one of the many promises of adopting a microservices architecture. By splitting up an application into smaller parts, organizations hope to allow more development teams to work autonomously, without conflicting with each other, and ultimately allow a successful product to grow faster.
The reality is unfortunately not as clear-cut. As organizations build more microservices, they increasingly experience integration issues. The symptoms can vary, from integration tests grinding the pipeline to a halt, to increasing change lead time, to APIs breaking changes that are only discovered in production, creating incidents and increasing rework.
This can all happen when you adopt a new architecture without adjusting your testing strategy.
Contract testing is a different way to test microservices. It can reduce the pain of integration tests by making each service independently testable.
Microservices integration hell
End-to-end integration environments (e.g., "staging") suffer from well-known limitations, regardless of the architecture under test.
Spinning up a full application to match a live environment is costly and requires constant attention, especially when it's a critical part of your deployment pipeline. End-to-end tests are slow to run, often brittle, and ultimately very inefficient at covering large combinations of paths through an application.
Microservices exacerbate these limitations. If you have 100 services, what combination of versions are under test in staging? What if multiple teams want to test a new microservice at the same time?
If each service is responsible for its own data, how is your test data being governed? Of course, mocking dependencies is an option, but who’s responsible for building those mocks and making sure they are up to date?
Beth Skurrie, co-founder of Pact, an open-source contract testing platform, called this the "testing pyramid of hell" in her talk, "Microservices: Test smarter, not harder," at Voxxed Days Australia 2019. Here's how she depicted it:
Figure 1. The testing pyramid of hell. (Credit: Beth Skurrie)
Contract testing: Better coverage, faster feedback
The integration points between microservices are where things can go wrong. If end-to-end tests aren't going to work at scale, what can you do to ensure that service interactions are properly tested?
Contract testing acknowledges the importance of service interactions in a distributed system. By formalizing these interactions in contracts, it aims to increase confidence that no breaking changes will be introduced.
Because contracts focus only on interactions between two services, not the entire application, they don't require you to run full integration environments. And that's why they can cover orders of magnitude more code paths than can end-to-end test scenarios.
Consider two services interacting over an HTTP API. The "consumer" service below is sending an HTTP request to the "provider," which returns the response.
In this case, the two teams responsible for each service must agree on the contract of this request/response interaction, which will include things such as the HTTP endpoint, request parameters, response body, and so on.
By agreeing to this contract, the two teams guarantee that the consumer will use the API only in this documented way, and that the provider will not change the API in a way that would break it.
Figure 2. The authentication and users' teams formalize the interaction between the services they own. The outcome is a contract that they can use in tests to prevent breaking changes. Credit: Pierre Vincent
[ Understand quality-driven development with best practices from QA practitioners in TechBeacon's Guide. Plus: Download the World Quality Report 2019-20 ]
Breaking testing down
To scale, the definition and verification of the contract on both sides cannot possibly happen manually. This is where Pact comes in. This consumer-driven contract testing framework facilitates this way of testing by separating it into multiple phases.
First, the consumer records its expected interactions (this makes up the contract between the two services, or "pact"). Then they share the pact with the provider, which will finally replay these interactions and verify that the API meets expectations.
Figure 3. The consumer (login service) starts by recording the expected interactions, then shares them through the pact broker. Finally, the provider (user service) replays the interactions to test the contract. Credit: Pierre Vincent
Sharing of the pacts is done via the pact broker (self-hosted or cloud-hosted on pactflow.io). It deals with service versioning, tagging (for environments), and validation statuses. The broker plays a central role in bringing all the testing phases together in a CI/CD setup.
Fast feedback and language support
The key difference with integration tests done in this manner is that all of this is happening at the unit test level of the testing pyramid. As a result, you can test hundreds of interactions in a few seconds, without the need to build, package, and deploy the services into a readily provisioned test environment.
This fast feedback is very useful for catching issues early. For example, developers will know when they are introducing a breaking change even before committing because the tests will fail locally on their machines.
The pact specification is also language-agnostic: the contracts are a JSON representation of the interactions and do not depend on the technology used for either the consumer or the provider. This is key for the polyglot nature of microservices, since teams have the flexibility to implement their services in different languages.
Pact supports a wide range of languages by providing libraries for both creating contracts and validating them. Other contract testing tools exist outside of Pact, such as Spring Cloud Contracts. They follow similar principles, and Spring Cloud Contracts even offers interoperability tools to convert and share contracts between it and Pact.
Bringing it all together in the CI/CD pipeline
When tens or hundreds of services are maintained by several teams, deployments happen multiple times a day. Automating contract testing in the CI/CD pipeline means that every time a team deploys a new version of a service, it can be confident that interactions between services haven't broken.
There are two things to automate in the pipeline:
- A new version of a consumer must ensure it is using APIs of production providers correctly.
- A new version of a provider must ensure it hasn't broken any pacts for consumers that are currently in production.
Figure 4. Whether a change is introduced by the consumer or the provider, the CI/CD pipeline has stages to share, retrieve, and validate contracts before code is deployed to production. Credit: Pierre Vincent
Versioning and other fun features
Automating these pipelines is made possible by the API capabilities of the pact broker. Besides using the API to push their generated contracts, consumers can version their contracts and thus map each contract to a specific code commit.
On top of versions, you can apply tags to track which versions you're currently deploying in each environment (e.g., dev, staging, prod-eu-west, prod-us-east, and more).
Through the use of web-hooks, the broker allows developers to define triggers when contracts are published or changed. When a consumer pushes a new contract, the broker can automatically notify the provider CI/CD pipeline to validate it and report the validation status to the broker.
Finally, the consumer can ask the broker for this validation status before going any further in the pipeline.
In addition to storing contracts, the broker is a great source of information about the application architecture and dependencies, since it makes visible every relationship between different services. The contract for each relationship is a piece of living documentation, with real-life API usage examples.
Figure 5. Because the pact broker stores each contracted interaction, it can provide a service dependency graph that is always up to date with reality. Credit: Pierre Vincent
Use contracts to foster inter-team collaboration
Although contracts help with the increased complexity of testing interactions in distributed systems, they can't work on their own. As with any technology or automation tool, contracts are not a substitute for team members talking to each other.
However, the systematic use of contracts to define interactions can act as a constant reminder that teams must collaborate when writing new APIs or discussing integration points.
At Poppulo, contracts have become the most common way to express API designs, through an example-based format. Our teams come together to agree on a new pact before any coding starts, and continuously refer to the pact as they implement the functionality.
All teams involved can keep working autonomously, with the confidence that automatically validating their work against the agreed-upon contract will catch any oversight or misunderstanding early on.
More often than not, the contract agreed to at the beginning needs some adjustments, but that's okay. It's much cheaper for developers to discuss adjustments during implementation than it is at later integration stages.
Get started now and you'll never look back
Adopting a microservices architecture without changing your integration testing strategy is not scalable. Contract testing is one way to relieve the pressure on heavy and brittle end-to-end environments, by covering more integration points with faster feedback.
It may seem daunting to throw away a whole set of integration tests, however slow or unreliable they may be, and replace them with contracts. So my advice is to start small.
Draw your architecture and figure out your consumers, your providers, and their integration points. Pick one interaction, think about what the contract may look like, and try implementing simple unit tests to generate a contract and validate it.
Finally, get teams onboard by showing that it works, and that it's possible to effectively test against breaking changes with fast feedback—and without end-to-end environments.
For more practical advice on contract testing with Pact, attend my talk at the Ministry of Testing's TestBash Manchester, which takes place October 2-3, 2019, in Manchester, U.K. Can't make it to England? I'm also speaking on "Changing Tires on a Moving Car: A Journey to Zero-Downtime Deployments" on November 7, 2019, at Agile + DevOps East in Orlando, Florida. The conference runs from November 4-7.
[ Understand the issues and risks that come with SAP modernization with TechBeacon's Guide. Download: Ensure SAP Modernization Success with DevOps ]