Serverless Testing
Expert guidance for testing serverless applications locally and in CI/CD pipelines
You are an expert in testing serverless applications locally and in CI/CD pipelines. You design testing strategies that combine fast unit tests with emulator-backed integration tests and targeted end-to-end validation against real cloud services. ## Key Points - Structure handlers as thin wrappers that parse events and call testable business-logic functions — test the logic independently of Lambda/Worker event shapes for maximum reuse and speed. - Use `sam local start-api --warm-containers EAGER` during development for near-instant hot-reload feedback without deploying to the cloud. - Localstack's free tier does not support all AWS services or all API behaviors with full fidelity — validate critical paths with real AWS services in a dedicated test account. ## Quick Example ```bash npm install -D vitest @types/aws-lambda ``` ```bash npm install -D miniflare ```
skilldb get serverless-skills/Serverless TestingFull skill: 236 linesServerless Testing — Serverless
You are an expert in testing serverless applications locally and in CI/CD pipelines. You design testing strategies that combine fast unit tests with emulator-backed integration tests and targeted end-to-end validation against real cloud services.
Core Philosophy
Serverless testing requires a layered strategy because the execution environment is owned by the cloud provider. You cannot fully replicate Lambda's invocation model, IAM policy enforcement, or event source mappings locally — so do not try. Instead, structure your code so that the cloud-specific adapter layer (the handler) is as thin as possible, and the business logic underneath is framework-agnostic, easily unit-tested, and reusable. The handler parses the event, calls your logic, and formats the response. That is all it should do.
The testing pyramid still applies, but the layers shift. Unit tests cover business logic functions with mocked dependencies and run in milliseconds. Handler-level integration tests use local emulators (SAM CLI, Miniflare, localstack) to verify the adapter layer works with realistic event payloads and service interactions. End-to-end tests run against a deployed staging environment to validate IAM permissions, event source mappings, and service integrations that emulators cannot replicate. Each layer catches different classes of bugs; skipping any layer leaves blind spots.
Mock fidelity is the silent killer of serverless test suites. A mock that returns a DynamoDB response in a slightly wrong shape, or an SQS event fixture with a missing field, creates tests that pass locally and fail in production. Combat this by generating event fixtures from real invocations (sam local generate-event), validating mock return types against AWS SDK type definitions, and running at least one integration test per event source against localstack or real services.
Anti-Patterns
- Testing only through mocks — An entire test suite built on mocked AWS services gives false confidence. The mocks encode your assumptions about how the service behaves, not how it actually behaves. A wrong assumption (incorrect error shape, missing pagination, different consistency behavior) passes tests but breaks production.
- Deploying to the cloud for every test iteration — Deploying a full SAM/CDK stack to test a code change takes minutes and costs money. Use local emulators (
sam local start-api, Miniflare, localstack) for rapid iteration and reserve cloud deployments for CI pipeline validation. - Hardcoded event payloads that drift from reality — Copy-pasting event JSON into test files and never updating it means tests validate against a stale contract. Generate fixtures from real events, version them alongside the code, and update them when the event source changes.
- No integration tests in CI — Running only unit tests in CI misses the most common serverless bugs: incorrect IAM permissions, wrong environment variable names, and event source mapping misconfiguration. Run at least localstack-backed integration tests on every pull request.
- Testing the framework instead of your logic — Writing tests that verify API Gateway returns a 200 for a valid request is testing AWS, not your code. Focus tests on your business logic: correct transformation of input data, proper error handling, and expected side effects.
Overview
Testing serverless applications requires strategies that account for managed services, event-driven invocations, and cloud-specific APIs. A robust testing approach combines fast unit tests with handler-level integration tests using local emulators and mocks, topped by end-to-end tests against deployed environments. Tools like SAM CLI, Miniflare, and localstack bridge the gap between local development and the cloud.
Setup & Configuration
Vitest setup for Lambda handlers
npm install -D vitest @types/aws-lambda
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts'],
},
},
});
SAM CLI local invocation
# Invoke a single function with an event
sam local invoke MyFunction -e events/api-event.json
# Start a local API Gateway
sam local start-api --port 3000 --warm-containers EAGER
# Generate sample events
sam local generate-event apigateway aws-proxy --method GET --path /items > events/api-event.json
Miniflare for Cloudflare Workers
npm install -D miniflare
// test/worker.test.ts
import { Miniflare } from 'miniflare';
describe('Worker', () => {
let mf: Miniflare;
beforeAll(async () => {
mf = new Miniflare({
scriptPath: './src/index.ts',
modules: true,
kvNamespaces: ['CACHE'],
d1Databases: ['DB'],
});
});
afterAll(async () => {
await mf.dispose();
});
it('returns 200 for /api/data', async () => {
const response = await mf.dispatchFetch('http://localhost/api/data');
expect(response.status).toBe(200);
});
});
Core Patterns
Unit testing Lambda handlers with mocked services
// src/handler.test.ts
import { describe, it, expect, vi } from 'vitest';
import { handler } from './handler';
// Mock the DynamoDB client
vi.mock('@aws-sdk/lib-dynamodb', () => ({
DynamoDBDocumentClient: {
from: () => ({
send: vi.fn().mockResolvedValue({
Item: { id: '123', name: 'Test Item' },
}),
}),
},
GetCommand: vi.fn(),
}));
describe('handler', () => {
it('returns an item by ID', async () => {
const event = {
httpMethod: 'GET',
pathParameters: { id: '123' },
} as any;
const result = await handler(event);
expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.name).toBe('Test Item');
});
it('returns 400 when ID is missing', async () => {
const event = { httpMethod: 'GET', pathParameters: null } as any;
const result = await handler(event);
expect(result.statusCode).toBe(400);
});
});
Integration testing with localstack
# docker-compose.yml
services:
localstack:
image: localstack/localstack:latest
ports:
- "4566:4566"
environment:
SERVICES: dynamodb,s3,sqs,lambda
DEFAULT_REGION: us-east-1
// test/integration.test.ts
import { DynamoDBClient, CreateTableCommand } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb';
const client = DynamoDBDocumentClient.from(
new DynamoDBClient({
endpoint: 'http://localhost:4566',
region: 'us-east-1',
credentials: { accessKeyId: 'test', secretAccessKey: 'test' },
})
);
beforeAll(async () => {
const raw = new DynamoDBClient({
endpoint: 'http://localhost:4566',
region: 'us-east-1',
credentials: { accessKeyId: 'test', secretAccessKey: 'test' },
});
await raw.send(new CreateTableCommand({
TableName: 'Items',
KeySchema: [{ AttributeName: 'id', KeyType: 'HASH' }],
AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'S' }],
BillingMode: 'PAY_PER_REQUEST',
}));
});
it('stores and retrieves an item', async () => {
await client.send(new PutCommand({
TableName: 'Items',
Item: { id: '1', name: 'Widget' },
}));
const result = await client.send(new GetCommand({
TableName: 'Items',
Key: { id: '1' },
}));
expect(result.Item).toEqual({ id: '1', name: 'Widget' });
});
Event payload testing with shared fixtures
// test/fixtures/events.ts
import type { APIGatewayProxyEvent } from 'aws-lambda';
export function makeApiEvent(overrides: Partial<APIGatewayProxyEvent> = {}): APIGatewayProxyEvent {
return {
httpMethod: 'GET',
path: '/',
pathParameters: null,
queryStringParameters: null,
headers: { 'Content-Type': 'application/json' },
body: null,
isBase64Encoded: false,
resource: '',
stageVariables: null,
requestContext: {} as any,
multiValueHeaders: {},
multiValueQueryStringParameters: null,
...overrides,
};
}
Best Practices
- Structure handlers as thin wrappers that parse events and call testable business-logic functions — test the logic independently of Lambda/Worker event shapes for maximum reuse and speed.
- Run unit tests and handler tests on every commit in CI; run localstack/emulator integration tests on pull requests; run full end-to-end tests against a staging environment before production deploys.
- Use
sam local start-api --warm-containers EAGERduring development for near-instant hot-reload feedback without deploying to the cloud.
Common Pitfalls
- Mocking every AWS service in unit tests gives false confidence — a mock that returns the wrong shape will let tests pass while production breaks. Supplement with integration tests against localstack or real services.
- Localstack's free tier does not support all AWS services or all API behaviors with full fidelity — validate critical paths with real AWS services in a dedicated test account.
Install this skill directly: skilldb add serverless-skills
Related Skills
AWS Lambda
Expert guidance for building, deploying, and optimizing AWS Lambda functions
AWS Step Functions
Expert guidance for orchestrating serverless workflows with AWS Step Functions
Cloudflare Workers
Expert guidance for building and deploying applications on Cloudflare Workers at the edge
Cold Start Optimization
Expert guidance for mitigating and optimizing cold start latency in serverless functions
Event Triggers
Expert guidance for building event-driven serverless architectures with S3, SQS, and EventBridge triggers
Serverless Databases
Expert guidance for using serverless databases like PlanetScale, Neon, and Turso in serverless applications