Shifting from traditional application testing to serverless TypeScript engineering is all about shifting your perspective: you stop testing a running server, and you start testing how your function responds to events and SDK states. Here is a simple, practical example for each testing, using modern AWS SDK v3 syntax.
1. Unit Testing: Jest + Mock Payloads & SDK Client Mocking
In unit tests, you don't call real AWS services. You pass a simulated API Gateway event to your handler, and you mock the AWS SDK so it returns predictable data instead of hitting live infrastructure.
The Code Under Test (src/handler.ts)
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
const ddbClient = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(ddbClient);
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
const userId = event.pathParameters?.id;
if (!userId) {
return { statusCode: 400, body: JSON.stringify({ message: 'Missing ID' }) };
}
// Fetch from DynamoDB
const result = await docClient.send(new GetCommand({
TableName: process.env.USERS_TABLE,
Key: { id: userId }
}));
if (!result.Item) {
return { statusCode: 404, body: JSON.stringify({ message: 'User not found' }) };
}
return {
statusCode: 200,
body: JSON.stringify(result.Item),
};
};
The Jest Unit Test (tests/unit.test.ts)
Instead of the legacy aws-sdk-mock, the modern standard for AWS SDK v3 is aws-sdk-client-mock.
import { handler } from '../src/handler';
import { APIGatewayProxyEvent } from 'aws-lambda';
import { mockClient } from 'aws-sdk-client-mock';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
const ddbMock = mockClient(DynamoDBDocumentClient);
describe('Lambda Handler Unit Test', () => {
beforeEach(() => {
ddbMock.reset();
process.env.USERS_TABLE = 'TestTable';
});
it('should return 200 and user data when user exists', async () => {
// 1. Mock the AWS SDK Behavior
ddbMock.on(GetCommand).resolves({
Item: { id: 'user-123', name: 'Alice' }
});
// 2. Shape the Mocked APIGatewayProxyEvent Payload
const mockEvent = {
pathParameters: { id: 'user-123' }
} as unknown as APIGatewayProxyEvent;
// 3. Execute handler directly
const response = await handler(mockEvent);
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual({ id: 'user-123', name: 'Alice' });
});
});
2. Integration Testing: LocalStack
Integration tests verify that your code accurately interacts with auxiliary services, without relying on real AWS. LocalStack spins up containerized versions of AWS services inside your CI/CD pipeline or local environment.
To test against LocalStack, your application code just needs to point its AWS Clients to the local Docker container URL (usually http://localhost:4566).
The Integration Test Configuration (tests/integration.test.ts)
import { DynamoDBClient, CreateTableCommand } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
import { handler } from '../src/handler';
import { APIGatewayProxyEvent } from 'aws-lambda';
// Point the client to LocalStack instead of real AWS
const localStackConfig = {
endpoint: 'http://localhost:4566',
region: 'us-east-1',
credentials: { accessKeyId: 'test', secretAccessKey: 'test' }
};
const ddbClient = new DynamoDBClient(localStackConfig);
const docClient = DynamoDBDocumentClient.from(ddbClient);
describe('DynamoDB Integration via LocalStack', () => {
beforeAll(async () => {
// Setup: Create the table in LocalStack before tests run
await ddbClient.send(new CreateTableCommand({
TableName: 'LocalUsersTable',
AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'S' }],
KeySchema: [{ AttributeName: 'id', KeyType: 'HASH' }],
BillingMode: 'PAY_PER_REQUEST'
}));
process.env.USERS_TABLE = 'LocalUsersTable';
});
it('should successfully read data actually written to LocalStack', async () => {
// Seed real data into the LocalStack DynamoDB container
await docClient.send(new PutCommand({
TableName: 'LocalUsersTable',
Item: { id: 'user-456', name: 'Bob' }
}));
const mockEvent = { pathParameters: { id: 'user-456' } } as unknown as APIGatewayProxyEvent;
// Run the handler — it will communicate directly with LocalStack
const response = await handler(mockEvent);
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body).name).toBe('Bob');
});
});
3. End-to-End (E2E) Testing: Newman
In E2E testing, your application is fully deployed to a staging environment. You treat it entirely as a black box—sending actual HTTP requests to the live API Gateway URL. Newman allows you to run Postman collections natively via the CLI.
The Testing Workflow
- You export a Postman collection containing your test assertions (
my-api-tests.json). - You execute it via your terminal or CI environment against the live URL.
# Execute your Postman test collection against your deployed AWS Staging endpoint
newman run ./tests/my-api-tests.json --env-var "baseUrl=https://xyz123.execute-api.us-east-1.amazonaws.com/staging"
4. Local Execution: AWS SAM Local
Before deploying to staging, you want to see how your Lambda executes inside an isolated Docker container mimicking the real AWS Lambda runtime environment. AWS SAM achieves this.
Step A: Define your event payload (events/mock-request.json)
{
"pathParameters": {
"id": "user-789"
}
}
Step B: Invoke the function via CLI
Run the SAM build command to compile your TypeScript down to JavaScript, then use sam local invoke to execute the function with your mock event.
# 1. Compile TypeScript and build resources
sam build
# 2. Locally invoke the function using the mock event
sam local invoke UserHandlerFunction --event events/mock-request.json
Hope youe find it helpful
Hash
Top comments (0)