How to Test JavaScript Lambda Functions?

This article will discuss the different options for testing your AWS Lambda functions; the focus will be on JavaScript.

Function as a service (FaaS) offerings like AWS Lambda are a blessing for software development. They remove many of the issues that come with the setup and maintenance of backend infrastructure. With much of the upfront work taken out of the process, they also lower the barrier to start a new service and encourage modularization and encapsulation of software systems.

Testing distributed systems and serverless cloud infrastructures specifically is always a source of long discussions. Some people prefer the local approach of emulating everything around your own code because it gives you fast iterations. Others say it gives you a false sense of safety because you’re not really testing the actual services involved later when you deploy into your production environment.

What Needs to be Tested?

First of all, your own code, obviously.

But the main part in the architecture where FaaS really shines is integration code. Lambda can be seen as versatile glue between all the managed services AWS, and other vendors, have to offer. So, the main focus of tests isn’t just your code but also how it integrates with different services. Having a Lambda that just reads an event and writes an output will be a rare occasion; usually, it will access one or multiple other services like S3, Step Functions, or RDS.

Smoke Tests

Smoke tests are a straightforward type of test. They only check that your code doesn’t crash when you try to run it. This means smoke tests don’t check if your code works correctly. It could be that you have a bug in some if-branch anywhere that isn’t executed with the test. It doesn’t test for logic issues either.

In terms of a web server, a smoke test would mean starting the server. No request gets sent to the server; just starting the server and see if it crashes. This is easy to do, and if it fails, you can save time running any other test.

For Lambda, the action of starting and handling an event is the same because Lambdas only run when they handle an event and get frozen or retired right after they did their work. This means a smoke test would mean sending an event to the Lambda function to see if it throws an error. The simplest even you think your Lambda function should be able to handle would do. 

A smoke test can be done via the AWS CLI with the following command:

$ aws lambda invoke \
--cli-binary-format raw-in-base64-out \
--function-name <LAMBDA_FUNCTION_NAME> \
--payload --payload file://<JSON_EVENT_FILE>

For automation purposes, you can add such CLI commands to a bash script and simply execute it before every other test runs.

Unit Tests

Unit tests are a bit more complex than smoke tests because they actually test the logic of your function. Since most errors usually happen when integrating your code with other services, they don’t bring that much value compared to integration tests.

But sometimes, you have very complex logic inside a Lambda function that doesn’t need to access other services. If it does access other services, the interaction with them is very basic.

To get unit tests going, your first step is extracting the logic you want to test into a JavaScript module. 

Let’s look at the following example of a Lambda function that adds or substracts two numbers depending on an operation argument.

exports.handler = async (event) => {
  if (event.queryStringParameters.operation === "substract")
    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body:
        event.queryStringParameters.x - 
        event.queryStringParameters.y
    }

  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body:
      event.queryStringParameters.x + 
      event.queryStringParameters.y
  }
}

This is a contrived example, but still, the function is harder to test than it needs to be. We would have to create an event object containing the queryStringParameters field, which would require an operation, x, and y fields to be present.

If we encapsulate this logic in a plain JavaScript function that only requires three arguments, things would be simpler. 

const addOrSubtract = (operation, x, y) => 
  operation === "substract" ? x - y : x + y;

exports.handler = async (event) => {
  const { operation, x, y } = event.queryStringParameters;

  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: addOrSubtract(operation, x, y)
  };
};

In this refactored example, we can now test the logic independently from the Lambda handler function.

Integration Tests

Integration tests are the most important part of testing FaaS. I said it before, and I will repeat it, AWS Lambda is mostly used to glue together managed cloud services, and the parts where your Lambda function interacts with other services are the most crucial test targets.

Now, there are two main ways of integration testing:

  • Test with real infrastructure
  • Test by emulating that infrastructure

They both have their pros and cons. For example, if testing with mocked-up infrastructure is faster and cheaper, but if your mocks are wrong, you’re tests are wrong too. Testing with real infrastructure gives you more confidence but costs more money and can be quite slow if you need to provide it for each test run.

Also, there is “no free lunch” in writing integration tests. The time you might save when you don’t have to meddle with real infrastructure will sink into keeping your mocked-up infrastructure up-to-date. Martin Fowler wrote an awesome article about everything that goes into mock tests.

Testing with Real Infrastructure

Testing with real infrastructure only makes sense when you are using infrastructure as code (IaC) tool. Otherwise, you waste too much time provisioning your resources manually. Especially serverless applications are prone to contain many small services.

AWS offers multiple IaC tools: CloudFormation, SAM, and the CDK are a few of them that are very well integrated with the AWS ecosystem. 

When you have your tool of choice ready, you can then use it to deploy to test and production with one IaC definition. This way, you can be sure your testing environment matches production.

Now, the tests would check the inputs and outputs of your Lambda functions.

For a synchronous invocation of Lambda, which happens with API-Gateway, for example, this means the events that go into your Lambda function and the response that function returns. For asynchronous invocations, there are no values returned.

The more interesting part of these tests is how your function accesses other services. If your function reads some data from DynamoDB for authentication, before it does its work, you need to check that that data is accessible and correct before running the test. If you write to S3, you must access S3 to check if everything went right after running the test.

You can use the same AWS SDK for JavaScript to check these services inside your tests. If you choose to run your tests on AWS Lambda, too, it will even be preinstalled.

Let’s look at how such an integration test could look like:

const aws = require("aws-sdk");

const dynamoDb = new aws.DynamoDB.DocumentClient();
const lambda = new aws.Lambda();
const s3 = new aws.S3();

exports.handler = async (vent) => {
  await firstTest();
};

async function firstTest() {
  await dynamoDb
    .put({
      TableName: "Users",
      Item: {
        HashKey: "userId",
        isAdmin: true
      }
    })
    .promise();

  await lambda
    .invoke({
      FunctionName: "createAdminFile",
      Payload: JSON.stringify({
  userId: "userId",
	  filename: "sample.txt",
  content: "OK" 
      })
    })
    .promise();

  const s3Object = await s3
    .getObject({
      Bucket: "admin-files",
      Key: "sample.txt"
    })
    .promise();

  checkFile(s3Object).contains("OK");

  await dynamoDb
    .delete({
      TableName: "Users",
      Key: { HashKey: "userId" }
    })
    .promise();

  await s3
    .deleteObject({
      Bucket: "admin-files",
      Key: "sample.txt"
    })
    .promise();
}

This example is a Lambda function that tests another Lambda function. It creates a user document in a DynamoDB table with admin permissions. Then it invokes a Lambda function with event arguments. After the function was invoked, it checks that a file in S3 was created. And finally, it cleans up all the test-related data.

This is only a basic implementation, including a testing framework like tape to make things more convenient. But it illustrates what even a simple integration test requires to work.

You can test, retest applications all you want but once that baby goes Live, s*#@ will happen. It’s just how it is. You’ll be able to use Dashbird‘s function view to see exactly how your application is behaving and when the app goes sideways, you’ll be able to use the Incident management platform you can see exactly what broke and where.

Conclusion

This article only talked about three basic methods to test your functions:

  • smoke tests
  • unit tests
  • integration tests.

There are even more test types out there that have a much bigger scope, like E2E tests or test specific behavior of your functions like performance tests.

To get started, you should be good to go with smoke and integration tests. Make sure your Lambda doesn’t crash right at the start of an invocation and then test that it actually accurately uses other services.

If you have very complex Lambda functions used for specific logic and not just to integrate multiple services, try to encapsulate that logic and run unit tests. This way, you can iterate faster and cheaper.


Further reading:

How to test serverless applications?

Log-based monitoring for AWS Lambda

10 mistakes to avoid when sizing your cloud resources

Why serverless apps fail and how to design resilient architectures?

Read our blog

ANNOUNCEMENT: new pricing and the end of free tier

Today we are announcing a new, updated pricing model and the end of free tier for Dashbird.

4 Tips for AWS Lambda Performance Optimization

In this article, we’re covering 4 tips for AWS Lambda optimization for production. Covering error handling, memory provisioning, monitoring, performance, and more.

AWS Lambda Free Tier: Where Are The Limits?

In this article we’ll go through the ins and outs of AWS Lambda pricing model, how it works, what additional charges you might be looking at and what’s in the fine print.

More articles

Made by developers for developers

Dashbird was born out of our own need for an enhanced serverless debugging and monitoring tool, and we take pride in being developers.

What our customers say

Dashbird gives us a simple and easy to use tool to have peace of mind and know that all of our Serverless functions are running correctly. We are instantly aware now if there’s a problem. We love the fact that we have enough information in the Slack notification itself to take appropriate action immediately and know exactly where the issue occurred.

Thanks to Dashbird the time to discover the occurrence of an issue reduced from 2-4 hours to a matter of seconds or minutes. It also means that hundreds of dollars are saved every month.

Great onboarding: it takes just a couple of minutes to connect an AWS account to an organization in Dashbird. The UI is clean and gives a good overview of what is happening with the Lambdas and API Gateways in the account.

I mean, it is just extremely time-saving. It’s so efficient! I don’t think it’s an exaggeration or dramatic to say that Dashbird has been a lifesaver for us.

Dashbird provides an easier interface to monitor and debug problems with our Lambdas. Relevant logs are simple to find and view. Dashbird’s support has been good, and they take product suggestions with grace.

Great UI. Easy to navigate through CloudWatch logs. Simple setup.

Dashbird helped us refine the size of our Lambdas, resulting in significantly reduced costs. We have Dashbird alert us in seconds via email when any of our functions behaves abnormally. Their app immediately makes the cause and severity of errors obvious.