Chapter 14 of 27 · Part 7: Testing

Testing strategy and unit tests

Why microservices need automated tests, how to balance them, and how to write good unit tests.

key idea / coordinatorservice / good pathin progress / watch outproblem / failureneutral

Why automate

FTGO tested by hand, after coding. Microservices make automated tests even more necessary.

The loop you wantEdit codechange Order svcRun testsone keystrokeSee resultin a few secondsRepeatstay in flowManual testing is slow and comes too late. Automated tests give this loop.

Testing basics

Every test sets up, executes, verifies and tears down. Doubles stand in for slow or missing dependencies.

Four phases of every testSetupbuild the fixtureExecutecall the SUTVerifyassert outcomeTeardownclean upSUT = System Under Test. It can be one class or the whole app.
Test doubles replace dependenciesStubReturns canned values to the SUTLets the test run fast and aloneMockAlso checks the SUT called ite.g. verify findById() was calledExample: test OrderController with a Mockito double for OrderService.
Four kinds of tests, by scopeUnitOne classfastestIntegrationAdapter + infraDB, other serviceComponentOne serviceacceptance testEnd-to-endWhole appslowest

Organizing tests

The quadrant says why a test exists. The pyramid says how many to write.

PurposeTechnology-facingBusiness-facing
Supports programmingUnit, integration (automated)Component, end-to-end (automated)
Critiques the productPerformance, non-functionalUsability, exploratory (manual)
The test pyramidEnd-to-endacceptance tests for the appComponentacceptance tests for one serviceIntegrationservice talks to its dependenciesUnitbusiness logicTop: slow, brittle, costly, so write few. Bottom: fast, reliable, cheap, so write many.

Test scope and balance

Newman splits tests by scope: what is switched on while the test runs. A wider scope gives more confidence, but costs speed and clarity. From Building Microservices, ch. 9.

What each test scope coversUnit testHelpdesk, web shop: outCustomer: one functionLoyalty, DB: outService testHelpdesk, web shop: outCustomer: whole serviceLoyalty: stubbedEnd-to-end testHelpdesk, web shop: inCustomer: inLoyalty: inExample system: helpdesk and web shop call Customer, which calls Loyalty.

A unit test calls one function and starts no services, so thousands run in under a minute. A service test starts one microservice and stubs what it calls. An end-to-end test drives everything, often through a browser.

Trade-off of moving up or down the pyramidWider scope (up)More confidence it all worksSlower feedbackHard to find the causeNarrow scope (down)Fast feedbackFailure points at the lineProves little about the wholeWhen a big test fails, write a small one that would have caught it sooner.

Newman avoids the word integration test because people mean different things by it. Rule of thumb: about ten times more tests with every step down.

Healthy pyramid versus test snow coneHealthy pyramidSnow cone (inverted)End-to-endEnd-to-endServiceServiceUnitUnitOne system had 4,000 unit, 1,000 service and 60 end-to-end tests: too many slow ones.A snow cone gives glacial builds, so a broken build stays broken for a long time.

The microservice challenge

Services talk over the network all the time. Contracts keep both sides honest without running them together.

Interactions that need testingRESTClient → serviceAPI gateway → OrderPub/subSubscriber ← publisherOrder History ← OrderAsync req/replyRequestor → replierOrder → KitchenEvery pair of services has a contract. Testing both together is an end-to-end test, which you want to avoid.
Consumer-driven contract testingConsumer teamwrites contractsPull requestto provider repoProvider buildruns all contractsFails?API broke: fix itContracts are example messages, not schemas: a sample request and the expected response.
Spring Cloud Contract: one contract, both sides testedAPI gw teamOrder svc teamMaven repoAPI gw testscontracts (PR)run generated testspublish contract JARdownload contractstest against stubThe provider runs generated tests; the consumer tests against a stub made from the same contract.

Service tests with stubs

A service test deploys one microservice, for example the container image from the build, and replaces every service it calls with a stub. If the test breaks, the fault is in that service. From Building Microservices.

A service test with a smart stubTestCustomer svcmountebankcreate imposter "Loyalty"GET customer 123GET balance, cust 123canned reply: 15,000customer + 15,000 pointsThe stub answers the same way whether it is called 0, 1 or 100 times.
StubMockSmart stub server
DoesReturns canned repliesReturns replies and fails the test if the call never cameA programmable fake you configure over HTTP
Cares how often called?NoYesYour choice
Best useDefault for service testsCheck a side effect, e.g. a points balance is createdFake several downstream services at once

Newman uses stubs far more than mocks in service tests, because overusing mocks makes tests brittle. Meszaros groups stubs, mocks, fakes, spies and dummies as test doubles.

mountebank: a small server you program over HTTP. You create imposters, each listening on a port with a protocol (HTTP, HTTPS, TCP or SMTP) and the replies to send. One instance can stand in for many services. It cannot fake message brokers, which is where Pact helps (next lesson).

The deployment pipeline

Each service gets its own automated path to production.

A deployment pipeline per serviceCommit stagecompile, unit, lintIntegrationadapters, contractsComponentservice in isolationDeployto productionEarly stages are fast feedback. Later stages are slower and closer to production.

Flaky tests and developer experience

More moving parts means more ways to fail for reasons unrelated to the feature: a service is down, the network glitches, a thread races or a timeout hits. A test that sometimes fails and is simply re-run is flaky. From Building Microservices.

Handling a flaky testFails at randompeople rerun itQuarantineout of the suiteFind the causethreads, networkFix or replacesmaller testFowler: track flaky tests down, and remove them from the suite if you cannot fix them now.

Why act fast? Teams drift into normalization of deviance: they get used to a suite that always fails like that, and real breakages hide in the noise. Sometimes the best fix is to change the software so it is easier to test.

Where developers run servicesRun for realServices your team ownsStubOther teams' servicesAvoidWhole system locallyCloud dev: slow loopFast local feedback beats running everything. Stubs fill the gaps.

As the number of services grows, running them all on one laptop hurts. Newman prefers that a developer runs only the few services they work on and stubs the rest. Moving development to the cloud lengthens the edit-and-test loop.

Writing unit tests

The base of the pyramid. Pick the style by the class's role.

Unit tests for each kind of classWhat to unit testStyle and exampleEntitiessociable · Order totalValue objectssimple · Money add/multiplySagassociable · CreateOrderSagaDomain servicessolitary · OrderServiceControllerssolitary · OrderControllerMessage handlerssolitary · OrderEventConsumer
Solitary versus sociableSolitary unit testClass under test onlyDependencies are mocksControllers, services, handlersSociable unit testClass plus real collaboratorsEntities, value objects, sagas
Unit testing a sagaTestCreateOrderSagaConsumer svcKitchen svcstart sagaValidateOrder cmdfake success replyCreateTicket cmdassert the commands sentTest the happy path and every rollback path. The test DSL fakes the database and broker.
Solitary test of a domain serviceTestOrderServiceMocksstub repositorycreateOrder()save orderpublish eventcreate sagaverify the calls

Unit testing with Jest

In practice, a unit test isolates code by mocking its dependencies: swapping the real function, object or library for a fake you control. This also removes slow work such as database queries, network calls and file access. From Bootstrapping Microservices, ch. 9.

Dependency injection makes mocking easyHard-codedsquare(n) calls multiply()Test cannot swap multiplyInjectedsquare(n, multiply)Test passes in a fakeDependency injection: hand dependencies in, instead of hard-coding them.
Unit test of the metadata microserviceJestruns index.test.jsTestmocks Express, DBService codestartMicroserviceFake librariesno server, no DBNothing real starts, so hundreds of such tests stay fast.

The test file sits beside the code as index.test.js. A describe block names the suite, each test runs the code and expect checks the result. The book tests that the service starts and that its /videos route reads from the database.

CommandWhat it does
npx jest or npm testRun all tests once
npx jest --watchLive reload: rerun tests when code changes
npx jest --initCreate the Jest config file
Design for testing: the book exports a startMicroservice function and only auto-runs it when the file is the entry point. Reshaping code so tests can call it is normal, and it is how test-driven development nudges design.

Tests as a gate in CI/CD

Tests only pay off when they run on their own. The pipeline runs them on every push, and production deployment waits for a green result. From Bootstrapping Microservices.

Automated tests gate deploymentPushcode and testsCI runs testsnpm testGatepass or failDeployonly when greenDocker and Docker Compose keep the test environment close to production.

Aim for hundreds or thousands of fast unit tests, fewer integration tests and very few end-to-end tests. Slow tests get skipped by developers. The line between test types is blurry: all tests sit on a spectrum.

Test independently: if teams deploy independently they should test independently, ideally in their own environment. Research summarized in Accelerate found high performers do most testing on demand, without a shared integrated environment (Building Microservices).

Recap

Quick check

You replace OrderService with a double and check that OrderController called findById() once. Stub or mock?
Show answer

A mock, because the test verifies the call. A double that only returned a value would be a stub. This is a solitary unit test.

A suite has 20 unit tests and 60 browser tests and takes three hours. What is the pattern called, and what is the first step?
Show answer

A test snow cone (inverted pyramid). Replace slow broad tests with smaller ones, delete duplicated coverage, and make the suite fast.