Node.js Serverless on AWS Lambda · Running Node.js Serverless in Production
Testing Lambda Functions
Why a test suite that only runs in the cloud slows every change, and how to structure Node.js functions for fast, reliable tests.
Scenario
Example app: the team's only tests deploy the whole stack to a shared AWS account and call the API. A full run takes 25 minutes, often fails because of other developers' deployments, and nobody runs it before pushing.
What you see in production
Simple bugs reach production regularly: a wrong field name in the event, an unhandled empty list, a missing permission. Developers say the handlers are "impossible to unit test" because they create AWS clients and call them directly.
The naive code
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});
export const handler = async (event) => {
const body = JSON.parse(event.body);
if (!body.items.length) return { statusCode: 400 }; // crashes when items is missing
await client.send(new PutItemCommand({ /* ... */ }));
return { statusCode: 201 };
};
Reproduce it
Separating business logic from the AWS adapter makes most behaviour testable in milliseconds:
// Business logic: no AWS imports, dependencies passed in.
function createOrderService(repo) {
return {
async create(input) {
if (!Array.isArray(input.items) || input.items.length === 0) return { status: 400, error: "items required" };
const total = input.items.reduce((s, i) => s + i.qty * i.price, 0);
await repo.save({ id: `ord-${input.items.length}-${total}`, total });
return { status: 201, total };
},
};
}
// Tests with an in-memory fake (a real test runner would use node:test or Vitest).
const saved = [];
const service = createOrderService({ save: async (o) => saved.push(o) });
const cases = [
["missing items", {}, 400],
["empty items", { items: [] }, 400],
["valid order", { items: [{ qty: 2, price: 10 }, { qty: 1, price: 5 }] }, 201],
];
for (const [name, input, expected] of cases) {
const result = await service.create(input);
console.log(result.status === expected ? "PASS" : "FAIL", name, JSON.stringify(result));
}
console.log("saved:", saved);
Output
PASS missing items {"status":400,"error":"items required"}
PASS empty items {"status":400,"error":"items required"}
PASS valid order {"status":201,"total":25}
saved: [ { id: 'ord-2-25', total: 25 } ]Root cause
When handlers mix event parsing, business rules and AWS SDK calls, the only way to exercise them is against real infrastructure, which is slow, flaky and shared. Test feedback then comes too late and too rarely to catch simple mistakes.
The fix
Use a testing pyramid adapted to serverless:
- Unit tests for business logic with dependencies injected (fakes or
aws-sdk-client-mockfor SDK v3 clients), usingnode:testor Vitest. Milliseconds per test. - Handler tests with real sample events (generate them with
sam local generate-eventor keep JSON fixtures) to verify parsing and response shapes for each event source. - Integration tests in the cloud against an ephemeral stack per branch or developer (
sam deployor CDK with a stack-name suffix), testing IAM permissions, event source mappings and service limits that emulators cannot reproduce. - Use
sam local invokeorsam local start-apifor quick manual checks, andsam sync --watchto iterate against real services.
// handler.mjs: a thin adapter
import { createOrderService } from "./orders.mjs";
import { dynamoOrderRepo } from "./repo.mjs";
const service = createOrderService(dynamoOrderRepo());
export const handler = async (event) => {
const result = await service.create(JSON.parse(event.body ?? "{}"));
return { statusCode: result.status, body: JSON.stringify(result) };
};
Trade-offs
- Mocks can drift from real service behaviour; keep a smaller set of cloud integration tests as the source of truth.
- Ephemeral stacks cost money and need cleanup automation.
- Local emulators (LocalStack, SAM local) speed iteration but do not reproduce IAM, quotas or scaling exactly.
Detect it in production
- Track escaped defects by category: logic bugs suggest missing unit tests; permission and integration bugs suggest missing cloud tests.
- CI duration and flakiness metrics for the test suites.
- Coverage of event fixtures for every event source a function handles.
Key points
- Keep handlers thin; test business logic without AWS.
- Test handlers with real sample events.
- Run integration tests in ephemeral cloud stacks for permissions and wiring.
Further reading: Testing serverless functions · Using sam local invoke
Practice questions
How do you make a Lambda handler unit-testable without AWS?
Answer
Keep the handler as a thin adapter that parses the event and calls business logic whose dependencies (repositories, clients) are passed in. Tests then use fakes or SDK client mocks and run in milliseconds.
What can only be tested reliably in a real AWS environment?
Answer
IAM permissions, event source mapping configuration, service quotas and limits, VPC and network access, and the real behaviour of managed services. Run these as integration tests in ephemeral stacks.
How do you get realistic test events for a handler?
Answer
Generate them with sam local generate-event for the event source (for example sam local generate-event sqs receive-message), or capture sanitised real events from logs, and keep them as fixtures.
Open in the app to mark lessons complete, take notes and track your progress.