Part III — Microservices and Reactive Architectures · Chapter 18

Testing Microservices

~50 min read5 interactive widgets7 plates

In this chapter

  1. About testing: purpose, SUT and the four phases of an automated test
  2. Test doubles: stubs and mocks
  3. The test pyramid
  4. The test quadrant
  5. Tests and the deployment pipeline
  6. Writing unit tests for a service
  7. Integration tests: persistence and IPC
  8. Consumer-driven contract testing
  9. Component tests and executable specifications
  10. End-to-end tests: user journeys
  11. Check your understanding

1. About testing: purpose, SUT and the four phases of an automated test

The purpose of a test is to verify the behaviour of the System Under Test (SUT) — here “verify” does not mean formal verification (e.g. model checking). The SUT is the software element being tested: it might be something as small as a class, as large as the entire application, or something in between, such as a cluster of classes or an individual service. A test case is a set of test inputs, execution conditions and expected results developed for a particular objective, such as exercising a particular program path or verifying compliance with a specific requirement; a test suite is a collection of related tests.

Automated tests are usually written using a testing framework — e.g. JUnit, a popular Java testing framework. Each test is implemented by a test method, which belongs to a test class, and consists of four phases:

  1. Setup — initializes the test fixture, everything required to run the test;
  2. Execute — invokes the SUT, e.g. a method on the class under test;
  3. Verify — makes assertions about the invocation’s outcome and the state of the SUT;
  4. Teardown — cleans up the test fixture, if necessary.

To reduce code duplication and simplify tests, a test class might have setup methods run before a test method and teardown methods run afterwards; the tests are executed by a test runner.

A TEST METHOD: FOUR PHASES 1. SETUP initialize the fixture 2. EXECUTE invoke the SUT 3. VERIFY assert outcome & state 4. TEARDOWN clean up the fixture setup/teardown methods reduce duplication · executed by a test runner · e.g. JUnit
Plate 18.1 — The four phases of an automated test: setup initializes the fixture, execute invokes the SUT, verify asserts on the outcome, teardown cleans up. Setup and teardown methods hoist the common work out of individual test methods.

2. Test doubles: stubs and mocks

An SUT often has dependencies — and the trouble with dependencies is that they can complicate and slow down tests. The solution is to replace the SUT’s dependencies with test doubles: an object that simulates the behaviour of the dependency. There are two types of test doubles:

The terms stubs and mocks are often used interchangeably, although they have slightly different behaviour — a mock is often a stub.

3. The test pyramid

The test pyramid organizes the strategies for testing microservices by granularity:

As we move up the pyramid we should write fewer and fewer tests — typically we have lots of unit tests and very few end-to-end tests.

THE TEST PYRAMID END-TO-END — acceptance, entire app COMPONENT — acceptance, one service INTEGRATION — service vs infrastructure/other services UNIT — a class, isolated few tests lots of tests
Plate 18.2 — The test pyramid: many unit tests at the base, fewer integration and component tests, and very few end-to-end tests at the top. Moving up means testing larger parts of the system with more moving parts.

4. The test quadrant

Marick’s test categorization quadrant classifies tests along two dimensions:

MARICK’S TEST QUADRANT support programming business facing technology facing Q1 · business facing support programming e.g. acceptance tests (ATDD) Q2 · technology facing support programming e.g. unit tests Q3 · business facing critique the product e.g. exploratory testing Q4 · technology facing critique the product e.g. performance tests
Plate 18.3 — Marick’s test quadrant categorizes tests along two dimensions: business-facing vs technology-facing, and supporting programming vs critiquing the application. The unit tests of the pyramid live in Q2; acceptance tests are the business-facing counterpart.

5. Tests and the deployment pipeline

Tests are injected in the service deployment pipeline — the automated process of getting code from the developer’s desktop into production, implemented using a Continuous Integration (CI) server such as Jenkins. The pipeline stages run the tests: unit and integration tests in the early stages (fast feedback), and the slower end-to-end tests closer to release.

TESTS INJECTED IN THE DEPLOYMENT PIPELINE commit developer build + unit tests fast feedback integration tests adapters & contracts staging e2e tests prod CI server (e.g. Jenkins) automates the path from the developer’s desktop to production
Plate 18.4 — Tests are injected into the service deployment pipeline run by a CI server: fast unit tests early, integration tests for adapters and contracts in the middle, and end-to-end tests in staging before production.

6. Writing unit tests for a service

Unit tests are the lowest level of the test pyramid: technology-facing tests that support development. A unit test verifies that a unit — a very small part of a service, typically a class — behaves as expected. There are two types of unit tests:

The responsibilities of the class and its role in the architecture determine which type to use. In a hexagonal-architecture service:

HEXAGONAL ARCHITECTURE & UNIT TESTS DOMAIN entities · value objects · sagas domain services sociable unit tests controllers (HTTP) message handlers repositories / DB brokers / external APIs solitary unit tests solitary unit tests domain logic in the middle: sociable tests · adapters at the edges: solitary tests with mocks
Plate 18.5 — The hexagonal architecture determines the unit-test strategy: domain objects (entities, value objects, sagas) are tested sociably; controllers, message handlers and domain services are tested solitarily with mocks for their dependencies.

7. Integration tests: persistence and IPC

Integration tests are the layer above unit tests in the testing pyramid: they verify that a service can properly interact with infrastructure services and other services — unlike end-to-end tests, they don’t launch services. Two main strategies: test each of the service’s adapters along with their supporting classes (e.g. the adapters realizing persistence), and test using contracts — a contract being a concrete example of an interaction between a pair of services.

Persistence integration tests verify that a service’s database access logic works as expected — services can use heterogeneous DB technologies (FTGO’s Order Service persists aggregates such as Order in MySQL using JPA; the Order History Service maintains a CQRS view in AWS DynamoDB). The phases mirror the test phases:

  1. Setup — set up the database by creating the schema and initializing it to a known state; it might also begin a database transaction;
  2. Execute — perform a database operation;
  3. Verify — make assertions about the state of the database and the objects retrieved from it;
  4. Teardown — optional; may undo the changes, e.g. by rolling back the transaction started by the setup phase.

IPC integration tests: interprocess communication plays an important role in a microservices-based application and is a main source of challenges for testing, because the application is a distributed system: teams are constantly developing their services and evolving their APIs, and services communicate using a variety of interaction styles and IPC mechanisms — some use request/response implemented with a synchronous protocol such as REST or gRPC, others publish/subscribe. Each interaction between a pair of services represents an agreement or contract between the two services — from the consumer service to the producer service, with the arrow pointing in the direction of the dependency (from the consumer of the API to the provider of the API).

8. Consumer-driven contract testing

A consumer contract test focuses on verifying that the “shape” of a provider’s API meets the consumer’s expectations. For a REST endpoint, a contract test verifies that the provider implements an endpoint that has the expected HTTP method and path, accepts the expected headers (if any), accepts a request body (if any), and returns a response with the expected status code, headers and body. Contract tests don’t thoroughly test the provider’s business logic. From a process point of view:

  1. the team that develops the consumer writes a contract test suite and adds it to the provider’s test suite;
  2. the developers of other services that invoke the provider also contribute a test suite — so each team that consumes Order Service’s API contributes a contract test suite verifying that the API matches its expectations;
  3. this test suite, along with those contributed by other teams, is run by the provider’s deployment pipeline — if a consumer contract test fails, that failure tells the producer team that they’ve made a breaking change to the API, and they must either fix the API or talk to the consumer team.

The interaction between a consumer and a provider is defined by a set of examples known as contracts: each contract consists of example messages exchanged during one interaction — for a REST API, an example HTTP request and response. Contracts are also used to verify that the consumer conforms to the contract, not only the provider: a consumer-side contract test for a REST client uses the contract to configure an HTTP stub service that verifies the request matches the contract and sends back the contract’s response. Testing both sides ensures that consumer and provider agree on the API. Contract testing frameworks: Spring Cloud Contract and the Pact family of frameworks.

In the FTGO full example, the API Gateway team writes contracts defining how the gateway interacts with Order Service; the Order Service team tests Order Service using the consumer contract tests, publishes the contracts that tested it to a Maven repository, and the API Gateway team uses the published contracts to write tests for the gateway. Contracts are refined depending on the interaction style: REST-based request–response (each contract is an HTTP request and reply), publish–subscribe (each contract specifies a domain event), and async request/response (each contract specifies the name of the command message channel and the structure of the command and reply messages).

CONSUMER-DRIVEN CONTRACT TESTING CONSUMER TEAMS write contract tests for their expectations PROVIDER’S PIPELINE runs all contributed contract test suites PUBLISHED CONTRACTS e.g. to a Maven repository CONSUMER TESTS with stubs from contracts contract tests contracts a failing contract test = a breaking change · frameworks: Spring Cloud Contract · Pact
Plate 18.6 — Consumer-driven contract testing: each consumer team contributes contract tests to the provider’s pipeline; the provider publishes the tested contracts, and consumers use them to test their own side with stubs. Both sides of every interaction are verified against the same contracts.

9. Component tests and executable specifications

Component testing verifies the behaviour of a service in isolation, writing the service’s acceptance tests: treating the service as a black box and verifying its behaviour through its API. It replaces the service’s dependencies with stubs that simulate their behaviour — it might even use in-memory versions of infrastructure services such as databases — so component tests are much easier to write and faster to run.

Acceptance tests are business-facing tests for a software component: they describe the desired externally visible behaviour from the perspective of the component’s clients rather than in terms of the internal implementation, and they are derived from user stories or use cases. In the FTGO example, the “Place Order” story of the Order Service is expanded into scenarios:

Given a valid consumer · Given using a valid credit card · Given the restaurant is accepting orders · When I place an order for Chicken Vindaloo at Ajanta · Then the order should be APPROVED · And an OrderAuthorized event should be published.

Each scenario defines an acceptance test: the givens correspond to the test’s setup phase, the when maps to the execute phase, and the then/and to the verification phase. Scenarios can be translated into code — an easier option is to write the acceptance tests using a DSL such as Gherkin (a DSL for writing executable specifications, with English-like scenarios), executed using Cucumber (a test automation framework for Gherkin), eliminating the need to manually translate scenarios into runnable code. A Gherkin specification for a service is a set of features, each described by a set of scenarios with the given–when–then structure; in Cucumber for Java, a step definition class defines the meaning of each step, with @Given mapping to setup, @When to execute, and @Then/@And to verification.

10. End-to-end tests: user journeys

End-to-end testing tests the entire application: a large number of moving parts, deploying multiple services and their supporting infrastructure services. A strategy for end-to-end testing is writing user journey tests: a user journey test corresponds to a user’s journey through the system — in the FTGO example, rather than test create order, revise order and cancel order separately, we write a single test that does all three. This approach significantly reduces the number of tests you must write and shortens the test execution time.

End-to-end tests must run the entire application, including any required infrastructure services. A supporting technology is Docker Compose: instead of running a single application service, the Docker Compose file runs all the application’s services — the natural complement to the container deployment pattern of Chapter 17.

USER JOURNEY TEST — ONE TEST, THREE STEPS create order revise order cancel order all services via Docker Compose one journey instead of three separate tests → fewer tests, shorter execution
Plate 18.7 — A user journey test follows the user through the whole application: create, revise and cancel an order in a single test, running the entire application (services plus infrastructure) with Docker Compose.

Check your understanding

What is the purpose of a test, and what are the four phases of an automated test?

The purpose of a test is to verify the behaviour of the System Under Test (SUT) — the software element being tested, from a class to the entire application (“verify” here does not mean formal verification such as model checking). A test case is a set of inputs, execution conditions and expected results; a test suite is a collection of related tests. Automated tests (e.g. JUnit) implement each test as a test method in a test class with four phases: setup (initialize the fixture), execute (invoke the SUT), verify (assert on the outcome and state), teardown (clean up). Setup/teardown methods reduce duplication; a test runner executes the tests.

What are stubs and mocks?

Test doubles replace the SUT’s dependencies to simplify and speed up tests. A stub returns values to the SUT; a mock is a test double that a test uses to verify that the SUT correctly invokes a dependency — it records the interactions and simulates the behaviour of the original system. The terms are often used interchangeably, though they have slightly different behaviour: a mock is often a stub.

Describe the test pyramid and the test quadrant.

The test pyramid: unit tests (a small part of a service, e.g. a class) at the base, then integration tests (service vs infrastructure services and other services), component tests (acceptance tests for an individual service), and end-to-end tests (acceptance tests for the entire application) at the top — fewer tests as we move up. Marick’s test quadrant categorizes tests along two dimensions: business facing vs technology facing, and supporting programming vs critiquing the application.

What are solitary and sociable unit tests, and when is each used?

A solitary unit test tests a class in isolation using mocks for its dependencies; a sociable unit test tests a class together with its dependencies. The class’s responsibilities and role in the architecture decide: controllers (adapters) and message handlers are tested solitarily (mocking services/repositories and stubbing the messaging infrastructure); domain services are tested solitarily (mocking repositories and messaging classes); entities, value objects and sagas are typically tested sociably. Controllers can be tested with Spring Mock MVC / Rest Assured Mock MVC making what appear to be HTTP requests.

What are the two strategies for integration tests, and what are the phases of a persistence integration test?

The two main strategies: test each of the service’s adapters along with their supporting classes (e.g. persistence adapters), and test using contracts — concrete examples of interactions between pairs of services. Persistence integration test phases: setup (create the schema, initialize to a known state, possibly begin a transaction), execute (perform a database operation), verify (assert on the database state and retrieved objects), teardown (optional — e.g. roll back the transaction). IPC integration tests verify interactions via the various styles (REST/gRPC request–response, publish/subscribe), where each interaction between a pair of services is an agreement/contract, with the arrow of dependency pointing from consumer to provider.

What does a consumer contract test verify, and how does the process work?

A consumer contract test verifies the “shape” of a provider’s API: for a REST endpoint, that the provider implements the expected HTTP method and path, accepts the expected headers and body, and returns the expected status code, headers and body — without thoroughly testing the provider’s business logic. Process: the consumer team writes a contract test suite and adds it to the provider’s test suite; all consumer teams contribute suites; the provider’s deployment pipeline runs them all, and a failure signals a breaking API change that the producer must fix or discuss with the consumers. Contracts also verify the consumer side (stubs configured from the contract). Frameworks: Spring Cloud Contract, Pact.

What are component tests, acceptance tests, and how do Gherkin and Cucumber work?

Component tests verify a service as a black box through its API, replacing dependencies with stubs (even in-memory databases), so they are easier to write and faster to run. Acceptance tests are business-facing tests describing the desired externally visible behaviour, derived from user stories or use cases; each scenario maps givens to setup, when to execute, then/and to verification. Gherkin is a DSL for executable specifications (features with given–when–then scenarios); Cucumber executes them — in Java, a step definition class maps @Given/@When/@Then/@And annotations to the test phases.

What are user journey tests, and how are end-to-end tests run?

End-to-end tests test the entire application with all its moving parts. User journey tests correspond to a user’s journey through the system — e.g. one test that creates, revises and cancels an order instead of three separate tests — significantly reducing the number of tests and shortening execution time. End-to-end tests must run the entire application including infrastructure services, typically using Docker Compose to run all the application’s services at once.