Testing Strategies For Microservices

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.

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:

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.

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.

  • 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.
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.

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.

  • 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.
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.

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.

Using integration tests to check that the microservices can communicate with other services, databases, and third party endpoints.

Component tests for microservices

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

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

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.

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.
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.

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.

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

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 are automated tests that simulate user interaction. Only external third-party services might be mocked.

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.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Semaphore

Semaphore

Supporting developers with insights and tutorials on delivering good software. · https://semaphoreci.com