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:
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.
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.
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.
Marick’s test categorization quadrant classifies tests along two dimensions:
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.
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:
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:
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).
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:
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.