Serverless Testing Strategies
Unit Testing Functions
Unit Testing Core Principles:
- Test individual function logic
- Isolate external dependencies
- Execute quickly
- Achieve high coverage
Testing Framework Choices:
- JavaScript/TypeScript: Jest, Mocha+Chai
- Python: pytest
- Java: JUnit
Testing Example (Jest):
// handler.test.js
const { handler } = require('../src/handler');
const AWS = require('aws-sdk-mock');
describe('Order Handler', () => {
beforeEach(() => {
AWS.mock('DynamoDB.DocumentClient', 'get', (params, callback) => {
callback(null, { Item: { id: '123', name: 'Test Order' } });
});
});
afterEach(() => {
AWS.restore('DynamoDB.DocumentClient');
});
it('should return order details', async () => {
const event = { pathParameters: { id: '123' } };
const result = await handler(event);
expect(result.statusCode).toBe(200);
expect(JSON.parse(result.body).name).toBe('Test Order');
});
});
Testing Techniques:
- Dependency Isolation:
- Use mock libraries to replace real services
- Override environment variables
- Testing Layers:
graph TD A[Unit Tests] --> B[Function Logic] A --> C[Input Validation] A --> D[Error Handling] E[Integration Tests] --> F[API Gateway] E --> G[Database Interaction] E --> H[Event Triggers] I[End-to-End Tests] --> J[Complete User Flow]
- Test Coverage:
# Jest configuration
"jest": {
"collectCoverage": true,
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
Integration Testing and Event Simulation
Integration Testing Focus:
- Inter-function interactions
- External service integrations
- Data flow validation
Event Simulation Tools:
- AWS EventBridge Simulation:
aws events put-events --entries '[
{
"Source": "test.source",
"DetailType": "TestEvent",
"Detail": "{\"key\":\"value\"}",
"EventBusName": "default"
}
]'
- S3 Event Simulation:
// Simulate S3 event
const s3Event = {
Records: [{
s3: {
bucket: { name: 'test-bucket' },
object: { key: 'test-file.txt' }
}
}]
};
- DynamoDB Stream Simulation:
{
"Records": [
{
"eventID": "1",
"eventName": "INSERT",
"dynamodb": {
"NewImage": {
"id": {"S": "123"},
"name": {"S": "Test Item"}
}
}
}
]
}
Integration Testing Example:
const AWS = require('aws-sdk');
const { handler } = require('../src/handler');
describe('Integration Tests', () => {
beforeAll(async () => {
// Initialize test data
await DynamoDB.putItem({ TableName: 'TestTable', Item: { id: '1', name: 'Test' } }).promise();
});
afterAll(async () => {
// Clean up test data
await DynamoDB.deleteItem({ TableName: 'TestTable', Key: { id: '1' } }).promise();
});
it('should process S3 event correctly', async () => {
const event = require('./events/s3-event.json');
const response = await handler(event);
expect(response).toBeDefined();
// Verify database changes
const data = await DynamoDB.getItem({ TableName: 'TestTable', Key: { id: '1' } }).promise();
expect(data.Item.name.S).toBe('Processed');
});
});
End-to-End Testing
E2E Testing Framework Choices:
- REST API: Postman, Newman, Supertest
- Full-Stack Apps: Cypress, Playwright
- Serverless-Specific: Serverless Artillery
Testing Process:
- Deploy test environment
- Prepare test data
- Execute user operation sequences
- Verify system state
- Clean up test data
Example (Cypress):
describe('Order Process', () => {
it('should complete order successfully', () => {
// 1. Visit product page
cy.visit('/products/123');
// 2. Add to cart
cy.get('.add-to-cart').click();
// 3. Checkout
cy.get('.checkout').click();
cy.fillForm({
name: 'Test User',
address: '123 Test St'
});
cy.get('.submit-order').click();
// 4. Verify order confirmation
cy.contains('Order Confirmed').should('be.visible');
cy.get('.order-id').then(($el) => {
const orderId = $el.text();
// Verify database
cy.verifyOrderInDB(orderId);
});
});
});
E2E Testing Strategies:
- Contract Testing: Verify APIs conform to contracts
- Scenario Testing: Cover main user journeys
- Performance Baseline: Establish performance benchmarks
- Regression Protection: Ensure new changes don’t break existing functionality
Serverless Debugging Techniques
Cloud Logs and Tracing
CloudWatch Logs:
# View function logs
aws logs tail /aws/lambda/my-function --follow
# Search specific logs
aws logs filter-log-events \
--log-group-name /aws/lambda/my-function \
--filter-pattern "ERROR" \
--start-time 1620000000000 \
--end-time 1620003600000
X-Ray Tracing:
const AWSXRay = require('aws-xray-sdk');
AWSXRay.captureAWS(require('aws-sdk'));
exports.handler = async (event) => {
const segment = new AWSXRay.Segment('handler');
try {
// Record subsegment
const dbSegment = segment.addNewSubsegment('database-query');
const data = await queryDatabase();
dbSegment.close();
return { data };
} catch (err) {
segment.addError(err);
throw err;
} finally {
segment.close();
}
};
Log Enhancement Techniques:
- Structured Logging:
logger.info('Order processed', {
orderId: '123',
status: 'shipped',
timestamp: new Date().toISOString()
});
- Correlation ID:
// Generate correlation ID
const correlationId = uuid.v4();
process.env.CORRELATION_ID = correlationId;
// Include correlation ID in all logs
logger.info('Processing request', { correlationId });
Local Debugging Tools
Serverless Offline:
serverless offline start --port 3000
SAM CLI:
sam local start-api --env-vars env.json
Debug Configuration (Visual Studio Code):
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Lambda",
"program": "${workspaceFolder}/node_modules/serverless/bin/serverless",
"args": [
"offline",
"start",
"--noTimeout",
"--port", "3000"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/**/*.js"]
}
]
}
Local Simulation Tools:
- LocalStack: Simulates AWS services
- DynamoDB Local: Local DynamoDB
- S3 Proxy: Local S3 simulation
Error Handling and Retries
Error Classification:
- Transient Errors: Network issues, temporary service unavailability
- Recoverable Errors: Validation failures, business rule conflicts
- Non-Recoverable Errors: Programming errors, invalid states
Retry Strategy:
async function withRetry(fn, retries = 3, delay = 1000) {
try {
return await fn();
} catch (err) {
if (retries <= 0 || !isTransientError(err)) throw err;
await new Promise(resolve => setTimeout(resolve, delay));
return withRetry(fn, retries - 1, delay * 2);
}
}
function isTransientError(err) {
return err.statusCode === 500 ||
err.code === 'ProvisionedThroughputExceededException' ||
err.message.includes('Timeout');
}
Dead Letter Queue (DLQ) Configuration:
functions:
processOrder:
handler: handler.processOrder
events:
- sqs:
arn: arn:aws:sqs:us-east-1:123456789012:orders-queue
batchSize: 10
onError: arn:aws:sqs:us-east-1:123456789012:dead-letter-queue
Testing Tools and Frameworks
Jest in Serverless
Jest Configuration:
// jest.config.js
module.exports = {
testEnvironment: 'node',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
collectCoverageFrom: ['src/**/*.{js,ts}'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
Jest Mock Example:
// __mocks__/aws-sdk.js
const mockDynamoDB = {
get: jest.fn().mockImplementation((params, callback) => {
callback(null, { Item: { id: '1', name: 'Mock Item' } });
})
};
const AWS = {
DynamoDB: {
DocumentClient: jest.fn(() => mockDynamoDB)
}
};
module.exports = AWS;
Test Coverage Report:
jest --coverage --reporters=default --reporters=jest-junit
SAM Local Testing
SAM Template Configuration:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello-world/
Handler: app.lambdaHandler
Runtime: nodejs14.x
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
Local Testing Commands:
# Start local service
sam local start-api --env-vars env.json
# Invoke function directly for testing
sam local invoke "HelloWorldFunction" -e event.json
# Generate CloudFormation template
sam package --template-file template.yaml --s3-bucket my-bucket --output-template-file packaged.yaml
Serverless Artillery Performance Testing
Installation and Configuration:
npm install -g serverless-artillery
Test Scenario Definition:
# load-test.yml
config:
target: "https://api.example.com"
phases:
- duration: 60
arrivalRate: 10
scenarios:
- name: "Get Orders"
flow:
- get:
url: "/orders"
Execute Test:
slsart invoke --stage test --path load-test.yml
Result Analysis:
{
"totalRequests": 600,
"failures": 15,
"latency": {
"min": 50,
"max": 300,
"median": 120,
"p95": 200
}
}
Advanced Features:
- Distributed Testing:
slsart invoke --stage test --path load-test.yml --workers 5
- Custom Metrics:
scenarios:
- name: "Order Creation"
flow:
- post:
url: "/orders"
json:
productId: 123
quantity: 2
- think: 1
- extract:
- name: "orderId"
regexp: '"id":"(.*?)"'
- CI/CD Integration:
# .github/workflows/load-test.yml
name: Load Test
on:
pull_request:
branches: [ main ]
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install -g serverless-artillery
- run: slsart invoke --stage test --path load-test.yml



