Have a look at our new "Complete Guide to Optimizing Slow Tests"! Discover →

20 Jul 2022 · Software Engineering

Testing Strategies For Microservices

10 min read
Contents

A microservice application is a group of distributed programs that communicate over networks, occasionally interfacing with third-party services and databases. Microservices, by their networked nature, provide more points of failure than a traditional monolith. As a result of this, we need a different, broader approach to testing.

So, how do we test a microservice application? Does the testing pyramid still work? How do we test when third party services are involved and network disruptions are a possibility? We’ll attempt to answer all these questions in this post.

The challenges of testing microservices

The microservice architecture is a paradigm shift so profound that we must reconsider conventional testing techniques. Microservices differ from the classic monolithic structure in many ways:

  • Distributed: microservices are deployed across multiple servers, potentially across geographical locations, adding latency and exposing the application to network disruptions. Tests that rely on the network can fail due to no fault of the code, interrupting the CI/CD pipelines and blocking development.
  • Autonomous: as long as they don’t break API compatibility, development teams are free to deploy their microservices at any time.
  • Increased test area: since each microservice exposes at least a few API endpoints, there are many more testable surfaces to cover.
  • Polyglot: development teams can choose the best language for their microservice. In a big system, it’s unlikely that we’ll find a single test framework that works for all components.
  • Production is a moving target: because microservices are independently-deployable and built by autonomous teams, extra checks and boundaries are required to assure they will all still function correctly together when deployed.

All these characteristics force us to think of new testing strategies.

The testing pyramid for microservices

The testing pyramid is a planning tool for automated software testing. In its traditional form, the pyramid uses three types of tests:

The microservice pyramid adds two new types: component and contract tests.

The testing pyramid for microservice architecture. At the base, we have unit tests, then contract tests, integration tests, component tests, and end-to-end tests at the top.
This is one version of the microservice testing pyramid. In others, the order may vary. Some may include contract tests in the integration layer. The truth is that the pyramid is more of a guideline, not something written in stone.

Let’s see how each pyramid layer works in further detail.

Unit tests for microservices

Unit tests are one of the most fine-grained — and numerous — forms of testing. A unit consists of a class, method, or function that can be tested in isolation. Unit testing is an inseparable part of development practices like Test-Driven Development or Behavior-Driven Development.

Compared to a monolith, a unit in a microservice has a much higher chance of requiring a network call to fulfill its function. When this happens, we can either let the code access the external service — accepting some latency and uncertainty — or replace the call with a test double, giving us two ways of dealing with microservice dependencies:

  • Solitary unit tests: this should be used when we need the test result to always be deterministic. We use mocking or stubbing to isolate the code under test from external dependencies.
  • Sociable unit tests: sociable tests are allowed to call other services. In this mode, we push the complexity of the test into the test or staging environment. Sociable tests are not deterministic, but we can be more confident in their results when they pass.

Sociable unit tests allow interaction between services. Test doubles mock the response from other services.
We can run unit tests in isolation using test doubles. Alternatively, we can allow tested code to call other microservices, in which case we’re talking about sociable tests.

As you’ll see, balancing confidence vs. stability will be a running theme throughout the entire post. Mocking makes things faster and reduces uncertainty, but the more you mock, the less you can trust the results. Sociable tests, despite their downsides, are more realistic. So, you’ll likely need to strike a good balance of both types.

If you want to check examples of solitary vs sociable tests, check out this nice post from Dylan Watson at dev.to.

Contract testing

A contract is formed whenever two services couple via an interface. The contract specifies all the possible inputs and outputs with their data structures and side effects. The consumer and producer of the service must follow the rules stated in the contract for communication to be possible.

Contract tests ensure that microservices adhere to their contract. They do not thoroughly test a service’s behavior; they only ensure that the inputs and outputs have the expected characteristics and that the service performs within acceptable time and performance limits.

Depending on the relationship between the services, contract tests can be run by the producer, the consumer, or both.

  • Consumer-side contract tests are written and executed by the downstream team. During the test, the microservice connects to a fake or mocked version of the producer service to check if it can consume its API.
  • Producer-side contract tests are run in the upstream service. This type of test emulates the various API requests clients can make, verifying that the producer matches the contract. Producer-side tests let the developers know when they are about to break compatibility for their consumers.

Producer contract tests run on the upstream service. Consumer contract tests run in the downstream side.
Contract tests can run on the upstream or downstream. Producer tests check that the service doesn’t implement changes that would break depending services. Consumer tests run the consumer-side component against a mocked version of the upstream producer (not the real producer service) to verify that the consumer can make requests and consume the expected responses from the producer. We can use tools such as wiremock to reproduce HTTP requests.

If both sides of the contract tests pass, the producers and consumers are compatible and should be able to communicate. Contract tests should always run in continuous integration to detect incompatibilities before deployment.

You can play with contract testing online in the Pact 5-minute getting started guide. Pact is a HTTP-based testing tool to write and run consumer- and producer-based contract tests.

Integration tests for microservices

Integration tests on microservices work slightly differently than in other architectures. The goal is to identify interface defects by making microservices interact. Unlike contract tests, where one side is always mocked, integration tests use real services.

Integration tests are not interested in evaluating behavior or business logic of a service. Instead we want to make sure that the microservices can communicate with one another and their own databases. We’re looking for things like missing HTTP headers and mismatched request/response pairings. And, as a result, integration tests are typically implemented at the interface level.

Integration tests verify communication channels between services, database, and third party APIs.
Using integration tests to check that the microservices can communicate with other services, databases, and third party endpoints.

Check out Vitaly Baum’s post on stubbing microservices to see integration code test in action.

Component tests for microservices

A component is a microservice or set of microservices that accomplishes a role within the larger system.

Component testing is a type of acceptance testing in which we examine the component’s behavior in isolation by substituting services with simulated resources or mocking.

Component tests are more thorough than integration tests because they travel happy and unhappy paths — for instance, how the component responds to simulated network outages or malformed requests. We want to know if the component meets the needs of its consumer, much like we do in acceptance or end-to-end testing.

Component testing performs end-to-end testing on a group of microservices. Services outside the scope of the component are faked.
Component testing performs end-to-end testing to a group of microservices. Services outside the scope of the component are mocked.

There are two ways of performing component testing: in-process and out-of-process.

In-process component testing

In this subclass of component testing, the test runner exists in the same thread or process as the microservice. We start the microservice in an “offline test mode”, where all its dependencies are mocked, allowing us to run the test without the network.

In-process component testing
Component test running in the same process as the microservice. The test injects a mocked service in the adapter to simulate interactions with other components.

In-process testing only works when the component is a single microservice. On a first glance, component tests look very similar to end-to-end or acceptance tests. The only difference is that component tests pick one part of the system (the component) and isolate it from the rest. The component is thoroughly tested to verify that it performs the functions its users or consumers need.

Compoment vs e2e testing
Component and end-to-end testing may look similar. But the differece is that end-to-end tests the complete system (all the microservices) in a production-like environment, whereas component does it on an isolated piece of the whole system. Both types of tests check the behavior of the system from the user (or consumer) perspective, following the journeys a user would perform.

We can write component tests with any language or framework, but the most popular ones are probably Cucumber and Capybara.

Out-of-process component testing

Out-of-process tests are appropriate for components of any size, including those made up of many microservices. In this type of testing, the component is deployed — unaltered — in a test environment where all external dependencies are mocked or stubbed out.

Out of process component testing
In this type of component tests the complexity is pushed out into the test environment, which should replicate the rest of the system.

To round out the concept of contract testing you may explore example code for contract testing on Java Spring. Also, if you are a Java developer, this post has code samples for testing Java microservices at every level.

End-to-end testing in microservices

So far, we have tested the system piecemeal. Unit tests were used to test parts of a microservice, contract tests covered API compatibility, integration tests checked network calls, and component tests were used to verify a subsystem’s behavior. Only at the very top of the automated testing pyramid do we test the entire system.

End-to-end (E2E) testing ensures that the system meets users needs and achieves their business objectives. The E2E suite should cover all the microservices in the application using the same interfaces that users would–often with a combination of UI and API tests.

The application should run in an environment as close as possible to production. Ideally, the test environment would include all the third-party services that the application usually needs, but sometimes, these can be mocked to cut costs or prevent abuse.

End-to-end testing microservices should simulate user journeys.
End-to-end are automated tests that simulate user interaction. Only external third-party services might be mocked.

As depicted by the testing pyramid, E2E tests are the least numerous, which is good because they are usually the hardest to run and maintain. As long as we focus on the user’s journeys and their needs, we can extract a lot of value with only a few E2E tests.

Conclusion

A different paradigm calls for a change in strategies. Testing in a microservice architecture is more important than ever, but we need to adjust our techniques to fit the new development model. The system is no longer managed by a single team. Instead, every microservice owner must do their part to ensure that the application works as a whole.

Some organizations might decide that unit, contract, and component tests are enough. Others, not content without end-to-end and integration testing, may choose to establish a QA team to facilitate cross-team test coverage.

Want to learn more about microservices? Check these articles:

Thanks for reading!

Leave a Reply

Your email address will not be published.

mm
Writen by:
I picked up most of my skills during the years I worked at IBM. Was a DBA, developer, and cloud engineer for a time. After that, I went into freelancing, where I found the passion for writing. Now, I'm a full-time writer at Semaphore.
Avatar for Tomas Fernandez
Reviewed by:
I picked up most of my soft/hardware troubleshooting skills in the US Army. A decade of Java development drove me to operations, scaling infrastructure to cope with the thundering herd. Engineering coach and CTO of Teleclinic.