Health check-ups — testing a human
Before a human starts work — and every time its DNA changes — give it a check-up. Testing a microservice has three layers, from fastest to most realistic.
The testing pyramid, medically
| Layer | Analogy | Tests | Speed |
|---|---|---|---|
| Unit tests | Testing reflexes | The brain's decisions with fake memory and voice | Milliseconds |
| Infrastructure tests | Genetic screening | The CDK DNA produces the right resources and settings | Seconds |
| Integration / end-to-end tests | A full physical | The deployed body answers real requests correctly | Seconds to minutes |
1. Reflexes: unit-testing the brain
Because the brain is pure (see the brain), its tests need no AWS at all — fast and reliable. Test the rules: totals, rejections of unknown items, state transitions.
2. Genetic screening: CDK assertions
CDK's assertions module checks the synthesised template — catching dangerous DNA before deployment.
// test/waiter.infra.test.ts
import { App } from "aws-cdk-lib";
import { Template, Match } from "aws-cdk-lib/assertions";
import { CafeStack } from "../infra/cafe-stack";
const template = Template.fromStack(new CafeStack(new App(), "TestCafe"));
test("the Waiter's memory is on-demand and can be restored", () => {
template.hasResourceProperties("AWS::DynamoDB::Table", {
BillingMode: "PAY_PER_REQUEST",
PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true },
});
});
test("the Waiter's memory survives stack deletion", () => {
template.hasResource("AWS::DynamoDB::Table", { DeletionPolicy: "Retain" });
});
test("brain cells run on Node.js 22, ARM, with tracing", () => {
template.hasResourceProperties("AWS::Lambda::Function", {
Runtime: "nodejs22.x",
Architectures: ["arm64"],
TracingConfig: { Mode: "Active" },
});
});
test("the Waiter has a pain receptor", () => {
template.hasResourceProperties("AWS::CloudWatch::Alarm", {
AlarmDescription: Match.stringLikeRegexp("Waiter is in pain"),
});
});
Run with npm test. These tests synthesise the stack (bundling functions with esbuild), so they also catch broken imports.
Add a test whenever a production incident teaches you something: "tables must retain", "functions must have timeouts below 30 s". The DNA learns from its history.
3. A full physical: integration tests
Deploy to a test environment, then exercise the real body:
// test/waiter.e2e.test.ts — runs against a deployed environment
const URL = process.env.FRONT_DOOR_URL!;
test("a customer can place and recall an order", async () => {
const placed = await fetch(`${URL}/orders`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ tableNumber: 7, items: [{ menuItemId: "coffee", quantity: 1 }] }),
});
expect(placed.status).toBe(201);
const order = await placed.json();
expect(order.total).toBe(120);
const recalled = await fetch(`${URL}/orders/${order.orderId}`);
expect(recalled.status).toBe(200);
});
test("the skin rejects nonsense", async () => {
const res = await fetch(`${URL}/orders`, { method: "POST", body: JSON.stringify({ tableNumber: -1, items: [] }) });
expect(res.status).toBe(400);
});
For event-driven behaviour, a common trick is a test listener: a rule in the test environment that copies events to a queue the test can read, so you can assert "an OrderPlaced event was announced".
Local check-ups
You can run handlers locally (they're just functions), use unit tests with fakes, or invoke deployed functions from your machine. Many teams find a personal sandbox account per developer — deploy in a minute, test against real services — simpler and more faithful than emulating AWS locally.
Test the brain with fakes, screen the DNA with assertions, and give the living body a full physical in a real environment. Each layer catches different illnesses.