Properly Unit Testing Lambda Functions — With an Actual Production Example
Tags: AWS, Nodejs, AWS Lambda, Testing

Properly Unit Testing Lambda Functions — With an Actual Production Example

Providing you with a summary on how mocking in Node.js works and an actual production example on how to unit test a Lambda function. Rcls Follow Nov 14, 2020 · 6 min read

Recently I’ve been working on multiple microservices. Some of these have been small Lambda functions, triggered either by SQS or SNS, and their job is to process the event, then dispatch it forward.

For a long time something always bothered me with Lambdas. I believe it was the fact that I couldn’t get my head around how they should be properly tested. It always seemed incredibly difficult to mock the dependencies, which I could not inject, so we developed routes in the code to get around that. That was way over a year ago.

As I had to work with them again, I decided I’ll take a few hours to actually read about how to mock those resources properly, and how it works under the hood.

Node.js mocking

No one actually discusses this topic on the web in detail. They simply refer you to use some mocking library. Either everyone knows how it works, or no one really cares, as long as the library works.

I come from a PHP background, and to mock anything you need to use dependency injection. This way the actual implementation is replaced by your mock object through that injection. You can use DI in JavaScript too, but with Lambda functions it’s not possible.

So since I can’t use dependency injection, how do those mocking libraries actually work?

I will refer you to this Node.js documentation page about how modules are cached. You can almost figure it out from there. It took me a while to find this information.

Creating a mocked moduled in memory before actual module gets called

To summarize: Node.js will use an existing modules from memory, if it already exists, which means we can insert a mocked object in there BEFORE the actual implementation is called thus ‘intercepting ’any calls to the real one.

You can see a gist of actual test files below and try out this behavior.

In the gist above, we initiate the real AWS.DynamoDB module from aws-sdk before we do any mocking, having the actual SDK capture the call and this unit test will fail, because it would actually be an integration test with no configurations.

In the gist below you initiate the AWS.DynamoDB mock object before we call the actual SDK, having the mock ‘intercept ’the call thus avoiding any real network calls.

So this was something that bothered me and I really had to dig in to figure out how the concept works before I could write proper unit tests for Lambdas.

So where’s this production example?

Right below. The example I’m using is actually a pretty basic AWS Simple Email Service bounce notification Lambda function, which handles the bounce notifications received from SNS, and sends out summarized messages to an SNS topic, which eventually get delivered to a client application to be handled.

If you haven’t used SES before, a bounce is a notification from the service, letting you know that the email delivery has failed, or ‘bounced’, for some reason. It can be that the email inbox is full, or unavailable, or there’s a transient error like a vacation message reply.

In this example, we have multiple environments using the same service and we don’t want to expose bounce messages to all of them, so we have a Lambda function to actually direct them to the right SNS topic

So, the Lambda function that is run in a production environment is something like this:

To run this inside Lambda, we need the aws-sdk package as a dependency, so that we have easy access to SNS and SQS functions, but I’m also using lodash here so I don’t have to write typeof checks everywhere. We usually use Serverless framework for deploying resources like an SQS DLQ (Dead Letter Queue) or SNS topics with these microservices, but I won’t go into that.

You might notice that there is no error monitoring here. That’s because we actually use Sentry to handle errors in Lambda functions , but to make this example simpler, I’ve stripped those pieces of code away. We also push notifications of DLQ additions to Sentry so we can simply monitor the function through one platform.

The thing to also note, is that the handler actually initializes the AWS.SNS and AWS.SQS objects inside our function. This is simply because otherwise aws-sdk-mock can’t mock those objects for testing. It is a very small change as normally you might initialize them at the top of your file.

Please read the package description below.

NB: The AWS Service needs to be initialised inside the function being tested in order for the SDK method to be mocked e.g for an AWS Lambda function example 1 will cause an error region not defined in config whereas in example 2 the sdk will be successfully mocked. Also note that if you initialise an AWS service inside a callback from an async function inside the handler function, that won’t work either.

- aws-sdk-mock README.md

Unit testing the function

So, into the unit testing portion! First, I usually want to identify all the execution paths the function can go into. In here we can see four of them:

The notification type is not Bounce , but a Complaint or Delivery which we don’t care about, so we just skips those.

, but a Complaint or Delivery which we don’t care about, so we just skips those. The email does not contain original headers (a configuration missing in SES), so it fails and sends the message to the DLQ as we can’t do anything with it.

The email does not contain the TargetARN address for SNS, which is client controlled, so it fails and sends the message to the DLQ as we can’t do anything with it.

The function processes the message and succeeds, sending the event payload to the SNS topic.

Each response is a 200 OK, because in all four cases we handle the message. If the function crashes, Lambda will retry the message a few times then automatically put it to a DLQ.

I grabbed actual SNS message examples from AWS docs, which I could then add as resources to my test folder, and assert against. I created four different variations of this, by either removing or adding some data, to match those execution paths.

The resulting test looks like this:

When you run this, you should have four passing tests.