This article covers how to effectively unit test Node.js AWS Lambda functions:
-
Why test Lambda functions: • Ensure code works correctly • Catch errors early • Improve reliability
-
Key testing steps:
- Set up testing environment
- Make functions testable
- Write unit tests
- Mock AWS services
- Test different function types
- Follow best practices
- Integrate with CI/CD
- Debug and fix issues
- Optimize performance
-
Mocking libraries: • aws-sdk-mock
-
Best practices: • Separate business logic • Use dependency injection • Create AWS service mocks • Measure test coverage • Organize test files
Testing Area | Key Points |
---|---|
HTTP handlers | Test methods, payloads, responses |
Event-driven | Test event processing, error handling |
Scheduled | Test timing, multiple runs |
Performance | Check metrics, load testing |
By following these testing approaches, you can build more reliable and maintainable Lambda functions.
Related video from YouTube
Setting up your testing environment
To test your AWS Lambda functions, you need to set up your testing environment. This section will show you how to do this step-by-step.
Picking a testing framework
For Node.js AWS Lambda functions, you can use Jest or Mocha as your testing framework. Here's a quick look at both:
Framework | What it is | Why use it |
---|---|---|
Jest | A testing tool made by Facebook | Easy to set up, works fast, has many built-in features |
Mocha | A popular testing tool for Node.js | Can be customized, works with both sync and async tests |
When choosing between Jest and Mocha, think about:
- What your project needs
- How big and complex your code is
- How much you want to customize your tests
- How easy it is to set up and use with your current tools
Pick the one that works best for you and your project.
Installing required tools
To set up your testing environment, you'll need to install:
- A testing framework (Jest or Mocha)
- A test runner (like
jest
ormocha
) - A mocking library (like
aws-sdk-mock
)
You can install these using npm or yarn:
npm install --save-dev jest aws-sdk-mock
or
yarn add jest aws-sdk-mock --dev
Check the docs for each tool to make sure you're using the right versions.
Setting up the test runner
After installing the tools, you need to set up the test runner. This helps you run your tests easily.
For Jest, create a jest.config.js
file with this setup:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
};
For Mocha, create a mocha.opts
file with this setup:
--require @babel/register
--require ts-node/register
--ui bdd
--colors
--reporter spec
--timeout 10000
Change these settings to fit your project's needs and structure.
Making Lambda functions testable
To test AWS Lambda functions well, you need to write them with testing in mind. This section shows you how to do this by:
- Splitting up your code
- Using dependency injection
- Creating fake AWS SDK calls
Splitting up your code
It's best to keep your business logic separate from the Lambda handler. This makes your code easier to test and maintain.
Here's an example:
// lambdaHandler.js
import { calculateTotal } from './businessLogic';
export const handler = async (event) => {
const total = await calculateTotal(event.items);
return { statusCode: 200, body: JSON.stringify({ total }) };
};
// businessLogic.js
export const calculateTotal = async (items) => {
return items.reduce((acc, item) => acc + item.price, 0);
};
In this example, lambdaHandler.js
handles the Lambda event, while businessLogic.js
does the actual work.
Using dependency injection
Dependency injection means passing in the things your function needs, instead of hard-coding them. This makes your code more flexible and easier to test.
Here's how it works:
// lambdaHandler.js
import { calculateTotal } from './businessLogic';
export const handler = async (event, dependencies) => {
const total = await calculateTotal(event.items, dependencies);
return { statusCode: 200, body: JSON.stringify({ total }) };
};
// businessLogic.js
export const calculateTotal = async (items, dependencies) => {
const taxRate = dependencies.taxRate;
const total = items.reduce((acc, item) => acc + item.price * (1 + taxRate), 0);
return total;
};
In this example, we pass dependencies
to the calculateTotal
function.
Creating AWS SDK mocks
When testing Lambda functions, you don't want to use real AWS services. Instead, you can create fake (mock) versions of AWS SDK calls. This makes your tests faster and safer.
Here's an example using the aws-sdk-mock
library:
// test/businessLogic.test.js
import { calculateTotal } from '../businessLogic';
import AWS from 'aws-sdk';
import { mock } from 'aws-sdk-mock';
beforeEach(() => {
mock('DynamoDB', 'getItem', (params, callback) => {
callback(null, { Item: { taxRate: 0.08 } });
});
});
afterEach(() => {
mock.restore('DynamoDB');
});
test('calculateTotal', async () => {
const items = [{ price: 10 }, { price: 20 }];
const total = await calculateTotal(items, { taxRate: 0.08 });
expect(total).toBe(30.8);
});
This example creates a fake DynamoDB.getItem
method that always returns a tax rate of 0.08. The test then uses this fake method to check if calculateTotal
works correctly.
Writing unit tests for Lambda functions
Basic test structure
A unit test for a Lambda function usually has three parts:
- Setup: Prepare the test environment
- Execution: Run the Lambda function
- Assertion: Check if the output is correct
Here's a simple example using Jest:
// tests/lambdaHandler.test.js
import { handler } from '../lambdaHandler';
describe('lambdaHandler', () => {
it('should return a successful response', async () => {
// Setup
const event = { /* test event data */ };
const context = { /* test context data */ };
// Execution
const response = await handler(event, context);
// Assertion
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ /* expected response body */ });
});
});
Testing synchronous functions
For synchronous functions, you can simply call the function and check the output. Here's an example:
// tests/businessLogic.test.js
import { calculateTotal } from '../businessLogic';
describe('calculateTotal', () => {
it('should return the correct total', () => {
const items = [{ price: 10 }, { price: 20 }];
const total = calculateTotal(items);
expect(total).toBe(30);
});
});
Testing asynchronous functions
For asynchronous functions, use a testing library that supports async testing, like Jest or Mocha. Here's an example with Jest:
// tests/asyncBusinessLogic.test.js
import { asyncCalculateTotal } from '../asyncBusinessLogic';
describe('asyncCalculateTotal', () => {
it('should return the correct total', async () => {
const items = [{ price: 10 }, { price: 20 }];
const total = await asyncCalculateTotal(items);
expect(total).toBe(30);
});
});
Working with callbacks and promises
When testing functions that use callbacks or promises, you need to handle the asynchronous nature of these operations.
Here's how to test a callback-based function:
// tests/callbackBusinessLogic.test.js
import { callbackCalculateTotal } from '../callbackBusinessLogic';
describe('callbackCalculateTotal', () => {
it('should return the correct total', (done) => {
const items = [{ price: 10 }, { price: 20 }];
callbackCalculateTotal(items, (err, total) => {
expect(err).toBeNull();
expect(total).toBe(30);
done();
});
});
});
And here's how to test a promise-based function:
// tests/promiseBusinessLogic.test.js
import { promiseCalculateTotal } from '../promiseBusinessLogic';
describe('promiseCalculateTotal', () => {
it('should return the correct total', async () => {
const items = [{ price: 10 }, { price: 20 }];
const total = await promiseCalculateTotal(items);
expect(total).toBe(30);
});
});
In the promise example, we use await
to wait for the promise to finish before checking the result.
Mocking AWS services
When testing Lambda functions, it's important to create fake versions of AWS services. This helps make your tests faster and more reliable.
Why use fake AWS services?
Using fake AWS services in your tests has several benefits:
Benefit | Explanation |
---|---|
Faster tests | No need to wait for real AWS services to respond |
More reliable | Tests aren't affected by network issues or AWS service problems |
Lower costs | Avoid paying for AWS service usage during testing |
You can use tools like Jest or Sinon.js to create fake AWS services for your tests.
Creating fake S3 operations
To test S3 operations, you can make fake versions of the S3
client and its methods. Here's an example using Jest:
// tests/s3Mock.js
import { S3 } from 'aws-sdk';
const s3Mock = {
putObject: jest.fn(),
getObject: jest.fn(),
deleteObject: jest.fn(),
};
jest.mock('aws-sdk', () => ({
S3: jest.fn(() => s3Mock),
}));
export default s3Mock;
You can then use this fake S3 client in your tests:
// tests/lambdaHandler.test.js
import { handler } from '../lambdaHandler';
import s3Mock from './s3Mock';
describe('lambdaHandler', () => {
it('should upload a file to S3', async () => {
s3Mock.putObject.mockResolvedValue({});
const event = { /* test event data */ };
const context = { /* test context data */ };
const response = await handler(event, context);
expect(s3Mock.putObject).toHaveBeenCalledTimes(1);
expect(s3Mock.putObject).toHaveBeenCalledWith({
Bucket: 'my-bucket',
Key: 'my-key',
Body: 'my-file',
});
});
});
Creating fake DynamoDB queries
For DynamoDB, you can create fake versions of the DynamoDB
client and its methods:
// tests/dynamoDBMock.js
import { DynamoDB } from 'aws-sdk';
const dynamoDBMock = {
get: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
};
jest.mock('aws-sdk', () => ({
DynamoDB: jest.fn(() => dynamoDBMock),
}));
export default dynamoDBMock;
Use the fake DynamoDB client in your tests like this:
// tests/lambdaHandler.test.js
import { handler } from '../lambdaHandler';
import dynamoDBMock from './dynamoDBMock';
describe('lambdaHandler', () => {
it('should retrieve an item from DynamoDB', async () => {
dynamoDBMock.get.mockResolvedValue({
Item: { /* test item data */ },
});
const event = { /* test event data */ };
const context = { /* test context data */ };
const response = await handler(event, context);
expect(dynamoDBMock.get).toHaveBeenCalledTimes(1);
expect(dynamoDBMock.get).toHaveBeenCalledWith({
TableName: 'my-table',
Key: { /* test key data */ },
});
});
});
Creating fake versions of other AWS services
You can use the same method to create fake versions of other AWS services, like API Gateway, SQS, and SNS. The main idea is to make fake versions of the service clients and their methods, then use these in your tests to control how the services behave.
Testing different Lambda function types
This section covers how to test various Lambda function types. We'll look at testing HTTP API handlers, event-driven functions, and scheduled Lambda functions.
Testing HTTP API handlers
When testing HTTP API handlers, focus on these key areas:
Area | What to Test |
---|---|
Request methods | GET, POST, PUT, DELETE, etc. |
Request payloads | Correct validation and error handling |
Responses | Status codes, headers, and body content |
Here's an example of testing an HTTP API handler with Jest:
// tests/httpHandler.test.js
import { handler } from '../httpHandler';
describe('httpHandler', () => {
it('should handle GET requests', async () => {
const event = { httpMethod: 'GET', path: '/users' };
const response = await handler(event);
expect(response.statusCode).toBe(200);
expect(response.body).toContain('users');
});
it('should handle POST requests', async () => {
const event = { httpMethod: 'POST', path: '/users', body: JSON.stringify({ name: 'John Doe' }) };
const response = await handler(event);
expect(response.statusCode).toBe(201);
expect(response.body).toContain('user created');
});
});
Testing event-driven functions
For event-driven functions, test these main areas:
Area | What to Test |
---|---|
Event payloads | Correct validation and error handling |
Event processing | Handling single and multiple events |
Error handling | Retry logic and error reporting |
Here's an example of testing an event-driven function with Jest:
// tests/eventHandler.test.js
import { handler } from '../eventHandler';
describe('eventHandler', () => {
it('should handle SNS events', async () => {
const event = { Records: [{ Sns: { Message: JSON.stringify({ foo: 'bar' }) } }] };
const response = await handler(event);
expect(response).toBeUndefined();
});
it('should handle SQS events', async () => {
const event = { Records: [{ body: JSON.stringify({ foo: 'bar' }) }] };
const response = await handler(event);
expect(response).toBeUndefined();
});
});
Testing scheduled Lambda functions
For scheduled Lambda functions, focus on these areas:
Area | What to Test |
---|---|
Schedule | Correct timing (cron and rate expressions) |
Function execution | Proper handling of multiple runs |
Error handling | Retry logic and error reporting |
Here's an example of testing a scheduled Lambda function with Jest:
// tests/scheduledHandler.test.js
import { handler } from '../scheduledHandler';
describe('scheduledHandler', () => {
it('should run on schedule', async () => {
const event = { source: 'aws.events', detail: { instance: 'i-0123456789abcdef0' } };
const response = await handler(event);
expect(response).toBeUndefined();
});
});
sbb-itb-6210c22
Best practices for Lambda function unit testing
Keeping tests separate
When writing unit tests for Lambda functions, it's important to keep each test separate. This helps make sure each test runs on its own, without affecting others. Here's how to do it:
Practice | Description |
---|---|
Use separate files | Put each test suite in its own file |
Give unique names | Name each test case differently |
Don't share test data | Use new data for each test |
Use mocking | Create fake versions of dependencies |
By keeping tests separate, you can find and fix problems more easily.
Using test doubles
Test doubles help you test your Lambda function without using real dependencies. Here are the main types:
Type | Purpose |
---|---|
Stubs | Replace dependencies with test versions |
Mocks | Check how your function works with dependencies |
Spies | Watch how your function uses dependencies |
Using test doubles helps you test your Lambda function in different situations.
Measuring test coverage
Checking how much of your code is tested is important. Here's how to do it:
- Use a tool like Istanbul or Jest to measure coverage
- Set a goal for how much of your code should be tested (like 80% or 90%)
- Look at coverage reports to see which parts need more testing
Measuring coverage helps make sure your Lambda function is well-tested.
Organizing test files
Keeping your test files organized makes them easier to find and use. Here's how:
- Put all test files in a separate folder
- Name test files the same way each time
- Use subfolders to group tests by what they're testing
Good organization makes it easier to keep your Lambda function up to date.
Example of a well-organized test file structure
Here's an example of how to organize your test files:
tests/
├── handlers/
│ ├── userHandler.test.js
│ ├── productHandler.test.js
│ └──...
├── services/
│ ├── userService.test.js
│ ├── productService.test.js
│ └──...
├── utils/
│ ├── logger.test.js
│ ├── auth.test.js
│ └──...
└──...
This structure groups tests by what they're testing, making them easy to find.
Continuous integration for Lambda tests
Adding tests to CI/CD pipelines
To add unit tests to your CI/CD pipeline, set up your CI tool to run tests automatically. Here's how to do it with common CI tools:
CI Tool | Steps |
---|---|
Jenkins | 1. Add Node.js plugin 2. Set up job for tests 3. Use npm test command |
GitHub Actions | 1. Make workflow file in .github/workflows 2. Set up Node.js 3. Run npm test |
AWS CodePipeline | 1. Create pipeline with source stage 2. Add build stage for tests 3. Add deploy stage for Lambda |
Running tests automatically
To run tests on every code commit, set up your CI tool to start when you push code:
CI Tool | How to Set Up |
---|---|
Jenkins | 1. Set job to start on push 2. Use webhook for new commits |
GitHub Actions | 1. Set workflow to start on push 2. Use on.push event |
AWS CodePipeline | 1. Set pipeline to start on push 2. Use CodeCommit or GitHub with webhook |
Dealing with test failures
When tests fail, act quickly to fix them. Here's what to do:
- Watch for failures: Check your CI tool's dashboard often
- Look into problems: Find out why each test failed
- Fix issues: Solve the problems and run tests again
- Stop future failures: Add more tests or improve your code
Debugging and fixing issues
Common Lambda testing problems
When testing AWS Lambda functions, you might face these issues:
Problem | Description |
---|---|
Timeouts | Function takes too long to run |
Dependency issues | External tools or libraries not working right |
Network problems | Trouble connecting to other services |
Mocking errors | Issues with fake versions of services |
How to debug test failures
When a test fails, follow these steps:
- Look at test logs for error messages
- Use a debugger to check the code step by step
- Check Lambda function logs for errors
- Make sure the test setup is correct
Tools for better test diagnostics
Here are some tools to help find and fix test problems:
Tool | What it does |
---|---|
AWS CloudWatch | Shows logs and numbers about your function |
AWS X-Ray | Gives details about how your function works |
Node.js debuggers | Help you look at your code closely |
Mocking libraries | Let you make fake versions of other services |
These tools can help you find and fix issues in your Lambda function tests more easily.
Performance considerations
Speeding up test execution
Fast test execution helps you update and deploy your code more quickly. Here are some ways to speed up your tests:
Method | Description |
---|---|
Use a fast test runner | Try different test runners like Jest to find the fastest one |
Run tests at the same time | Use parallel testing to run multiple tests at once |
Choose a small test framework | Smaller frameworks like Ava can run tests faster |
Use fake dependencies | Replace real dependencies with fake ones to save time |
Balancing depth and speed
It's important to have both fast and thorough tests. Here's how to balance them:
- Test the most important parts of your code first
- Focus on areas that are likely to break
- Use the testing pyramid:
- Many unit tests
- Fewer integration tests
- Even fewer end-to-end tests
Testing Lambda performance
Besides checking if your Lambda function works correctly, you should also test how well it performs:
Method | Description |
---|---|
Use AWS Lambda metrics | Check built-in metrics like invocation count and error rate |
Try third-party tools | Use tools that give more detailed performance information |
Do load testing | Use tools like Apache JMeter to test how your function handles many requests |
Conclusion
Key takeaways
Unit testing AWS Lambda functions helps make sure your serverless apps work well. Here are the main points from this article:
Point | Description |
---|---|
Separate logic | Keep business logic apart from the Lambda handler |
Use dependency injection | Makes code easier to test |
Create AWS SDK mocks | Test without real AWS services |
Write different test types | Test both sync and async functions |
Use testing tools | Try Jest or Ava for writing and running tests |
Check test coverage | Use tools like Istanbul to see what needs more testing |
Why good testing matters
Good testing helps keep your code working well. Here's why it's important:
Benefit | Explanation |
---|---|
Find problems early | Spend less time fixing bugs later |
Better code | Fewer issues and easier to update |
Works as expected | Make sure your app does what it should |
Fewer new bugs | Lower chance of breaking things when you change code |
Team work | Everyone understands how the code should work |
FAQs
Can you unit test Lambda?
Yes, you can unit test AWS Lambda functions in Node.js. Here's how:
- Import the handler function into your test file
- Use a testing framework like Jest to write and run tests
Here's a simple example:
import { handler } from '../src/arcbot-stack.stream';
test('userInput: How is the weather?', async () => {
const event = {
userInput: 'How is the weather?'
};
const response = await handler(event, '');
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual({
respond: 'I do not understand. Please rephrase!'
});
});
This test:
- Imports the handler function
- Creates a mock event
- Calls the handler with the event
- Checks the response
Step | Description |
---|---|
Import | Bring in the Lambda function |
Mock | Create fake input data |
Run | Call the function with test data |
Check | Make sure the output is correct |