The Architecture Reference

Cld serverless · Cloud & SaaS · Intermediate

Testing Serverless

Serverless needs a novel test approach: don't re-couple decoupled services — aim for maximum confidence in minimum time across business logic, integration points, and data contracts.

Cld serverless Intermediate ⏱ 4 min read Complete

🧭 Analogy

You don’t crash-test a whole car to check the seatbelt buckle. You test the buckle in isolation, verify it bolts to the right mount, confirm it meets the safety spec — then run a few full crash tests on the journeys that matter most. Serverless testing works the same way: test the parts, verify the joins, and reserve end-to-end runs for critical paths.

Why serverless testing is different

The very attributes that make serverless powerful — decoupling, event-driven communication, managed services, distribution, cloud-native execution — make the traditional “deploy the whole system and test it end-to-end” strategy counterproductive. The core thesis: if your services are decoupled in development and at runtime, do not re-couple them during testing. Difficulty comes from variable latency, acknowledgment-only async communication, opaque AWS-owned services, eventual consistency, and the hardness of local emulation.

Rather than chasing maximum coverage or zero bugs (bugs are inevitable — “everything fails all the time”), the aim is maximum confidence in minimum time.

The serverless square of balance

graph TD
T["Test"] --- D["Deliver"]
D --- O["Observe"]
O --- R["Recover"]
R --- T
C["Maximum confidence,<br/>minimum time"] --> T
C --> O
C --> R

Keep test, deliver, observe, recover in equilibrium. “Stability is a product of speed” — so trade some pre-deployment coverage for observability and recovery. Distinguish critical paths (user present, time-sensitive, synchronous, business-critical data → test coverage + alerting) from noncritical paths (background, recoverable → alerting + fault tolerance, can ship more bugs within error budgets).

Three building blocks to test

An event-driven app has three things worth testing:

  • Business logic — abstract it from the handler and unit-test it in isolation (TDD).
  • Integration pointsConfiguration (→ infrastructure + unit tests), Permissions (→ infrastructure tests, e.g., asserting IAM via CDK Template.hasResourceProperties), and Payloads (→ unit, contract, and static analysis).
  • Data contracts — verify requests/responses against agreed types via contract testing (JSON Schema with Ajv, or Pact) and static typing.
graph TD
BL["Business logic"] --> UT["Unit tests (TDD)"]
IP["Integration points"] --> IT["Infrastructure + unit tests"]
IP --> CT["Contract tests"]
DC["Data contracts"] --> CT
DC --> SA["Static analysis"]

Don't test what AWS owns

“If you can’t fix it, you shouldn’t test it.” Don’t write tests that exercise AWS’s internal behavior. Test your configuration, permissions, and payloads — for example verify an EventBridge rule’s pattern with TestEventPatternCommand without invoking the underlying services. Mock any code you are not responsible for testing or fixing.

Just enough, just in time

  • Just enough testing — prefer static testing (unit tests + static analysis, no deployment); use mutation testing to find useless tests; remove flaky tests cautiously.
  • Just-in-time testing — limit the number of test runs; run as few times as possible, as close to production as possible — a shift-right posture tied to continuous integration (integrate to trunk at least daily).
  • Environments — use as few as possible. Avoid per-PR ephemeral environments (the “antithesis of CI”); prefer instant environments isolated to two or three components.
  • Post-deploy — add synthetic canaries and load test critical paths before big events.

Two supporting tools: a Definition of Done checklist (quality measure, release method, failure detection pre- and post-prod, debuggability/tracing, recovery) and FMEA (Failure Modes and Effects Analysis): probability × severity, adjusted by detection, to prioritize where tests and observability go.

Key insight

Treat a serverless system as a collection of distinct applications and apply quality controls per component. The async equivalent of an end-to-end assertion is a retried assertion with a timeout — e.g., “order placed → an orderCreated event appears on the bus.”

See also

When to use it — and when not

✅ Reach for it when

  • Testing distributed, event-driven systems built from managed services
  • You want to balance pre-deploy coverage against observability and recovery
  • Focusing quality effort on critical, time-sensitive, user-facing paths

⛔ Think twice when

  • Trying to deploy the whole system and test it end-to-end as one unit
  • Chasing maximum coverage or zero bugs (bugs are inevitable)
  • Testing what AWS owns — 'if you can't fix it, you shouldn't test it'

Check your understanding

Score: 0 / 4

1. What is the goal of serverless testing?

Because bugs are inevitable, serverless testing trades some pre-deploy coverage for observability and recovery, focusing on critical paths.

2. What are the three building blocks of an event-driven app to test?

Business logic → unit tests; integration points (configuration, permissions, payloads) → infrastructure/unit/contract tests; data contracts → static analysis and contract tests.

3. What is the 'serverless square of balance'?

Stability is a product of speed: trade some pre-deployment coverage for stronger observability and recovery.

4. Why should you generally not test what AWS owns?

Under shared responsibility, test your business logic, configuration, permissions, and payloads — verify config without invoking the underlying service.

Comments

Sign in with GitHub to join the discussion.