Node.js Unit Testing for AWS Lambda Functions

published on 30 July 2024

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:

    1. Set up testing environment
    2. Make functions testable
    3. Write unit tests
    4. Mock AWS services
    5. Test different function types
    6. Follow best practices
    7. Integrate with CI/CD
    8. Debug and fix issues
    9. Optimize performance
  • Testing frameworks:JestMocha

  • 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.

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 or mocha)
  • 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:

  1. Splitting up your code
  2. Using dependency injection
  3. 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:

  1. Setup: Prepare the test environment
  2. Execution: Run the Lambda function
  3. 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:

  1. Look at test logs for error messages
  2. Use a debugger to check the code step by step
  3. Check Lambda function logs for errors
  4. 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:

  1. Import the handler function into your test file
  2. 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

Related posts

Read more