Testing AWS Lambda with AVA

AWS Lambda functions are the future, so we need a futuristic test runner.

Max Gallo
DAZN Engineering
7 min readApr 27, 2017

--

In this post I'll show you how I used the “Futuristic JavaScript test runner” AVA to test AWS Lambda functions.

I started with a very basic Lambda function and the relative tests, and then I iterated over it, adding every time a new feature.

You can find everything described in this article in this repository.

WORLD 1–1

The Beginning

WORLD 1–2, The Beginning

In the world-1–1 folder you can find the very first Lambda function in a file called index.js.

exports.handler = (event, context, callback) => {
callback(null, `Hello from Lambda`);
};

This is the simplest Lambda function I could think of. This is how the test file, index.test.js, looks like

import test from 'ava';
import { handler } from './index';
test('Lambda is returning `Hello` message', t => {
const event = {};
const context = {};
const callback = (err, message) => {
t.is(message, 'Hello from Lambda');
}
handler(event, context, callback);
});

The code it’s really straightforward, When you run it, it succeeds!

Successful first test

WORLD 1–2

Time to go Async

WORLD 1–2, Time to go Async

Life is not only synchronous isn’t it? That’s why I added setTimeout in the Lambda body to simulate a real world asynchronous task.

exports.handler = (event, context, callback) => {
setTimeout( () => {
callback(null, 'Hello from Lambda');
}, 2000);
};

The function is working fine on AWS, but when I run the same test file from the world 1–1, the test fails. Could you tell why?

Running the tests I also noticed that they were too fast, it’s clearly not waiting for the setTimeout and that’s because we didn’t tell AVA about it.

AVA does natively support async function and it’s very straightforward to implement it. This is how our test, now, is perfectly handling async Lambda functions.

import test from 'ava';
import { handler } from './index';
const executeLambda = (event, context) => (
new Promise((resolve, reject) => {
handler(event, context, (err, response) => {
if (err !== null) {
return reject(err);
}
resolve(response);
});
})
);
test('Async Lambda is returning `Hello` message', async t => {
const event = {};
const context = {};
const response = await executeLambda(event, context);
t.is(response, 'Hello from Lambda');
});

I wrote a small function to help me “promisefy” the execution of the Lambda code. That function is able to take care of synchronous functions as well (it works for world-1–1). At this point I just added the async/await functions to make it work.

Notice how AVA is telling me that the second test took (2s) to run!

WORLD 1–3

Make it Fail

WORLD 1–3, Make it Fail

At this stage we’re going to force our Lambda to fail and we’ll see how to handle that with AVA. This is the index.js file this time

exports.handler = (event, context, callback) => {
setTimeout( () => {
const error = new Error('Something went wrong');
callback(error);
}, 2000);
};

We can handle that in AVA by using the throws assertion

test('Failing Async Lambda is throwing an error', async t => {
const event = {};
const context = {};
const executePromise = executeLambda(event, context);
const error = await t.throws(executePromise);
t.is(error.message, 'Something went wrong');
});

I’m doing two assertions in this test: I’m checking that the promise is throwing something and that the error message is exactly the string I wanted it to be. Tests look good so far

I’d like to point out that the order of the tests is not always the same. This is due to the nature of how AVA runs the tests

Even though JavaScript is single-threaded, IO in Node.js can happen in parallel due to its async nature. AVA takes advantage of this and runs your tests concurrently, which is especially beneficial for IO heavy tests. In addition, test files are run in parallel as separate processes, giving you even better performance and an isolated environment for each test file.

WORLD 1–4

But our princess is in another castle

WORLD 1–4, But our princess is in another castle

So fare we only used a single file to handle the function, but AWS allows us to use require for importing modules. You can require modules from local files and from the npm registry.

To use them you have to upload a Deployment Package directly in AWS Lambda console if it’s less than 10MB, on S3 otherwise. This package is just a zip file containing the necessary JavaScript files. Follow the official guide to learn more about how to create one.

In our example I’m going to use a module inside a local file, because they’re usually the ones you’ll want to test. Writing the tests in this “module” configuration will look very familiar to you, you simply write unit test for each module. Lets start with index.js

const getRandomNumber = require('./getRandomNumber');exports.handler = (event, context, callback) => {
setTimeout( () => {
const randomNumber = getRandomNumber();
callback(null, `Here's your number: ${randomNumber}`);
}, 2000);
};

Here I’m just importing the getRandomNumber method and using it into the callback response message.

This is the self-explanatory one line module getRandomNumber.js

module.exports = () => Math.random();

The two test files are not complicated at all, we got index.test.js where I used a .regex assertion to match the response from the Lambda

test('Random Async Lambda is throwing an error', async t => {
const event = {};
const context = {};
const response = await executeLambda(event, context);
t.regex(response, /^Here's your number: [\d]\.[\d]+$/);
});

and getRandomNumber.test.js where I compared two responses of the same random function with the .not assertion

import test from 'ava';
import getRandomNumber from './getRandomNumber';
test('Random function returns different results', t => {
const result1 = getRandomNumber();
const result2 = getRandomNumber();
t.not(result1, result2);
});

BONUS LEVEL

Welcome to the Warp Zone!

In addition to the things we did from WORLD 1–1 to WORLD 1–4, there are a few more things regarding testing AWS.

AWS SDK

In Lambda functions you have access to a wide set of functionalities via the module aws-sdk. You can do things like invoke another AWS Lambda, interact with DynamoDB, Decrypting with KMS Environment Variables and much more. Not sure the documentation is the right place where to start looking at, but it’s very detailed.

In case you want to mock that library there’s a super useful aws-sdk-mock library that could help, lets see how to use it.

Inside the bonus-level folder I wrote a decrypt.js file that uses aws-sdk to decrypt a Environment Variable passed as parameters.

const AWS = require('aws-sdk');
let DECRYPTED;
module.exports = encrypted => (
new Promise((resolve, reject) => {
if (DECRYPTED !== undefined) {
return resolve(DECRYPTED);
}
const kms = new AWS.KMS();
kms.decrypt(
{
CiphertextBlob: new Buffer(encrypted, 'base64')
},
(err, data) => {
if (err) {
reject(err);
}
DECRYPTED = data.Plaintext.toString('ascii');
resolve(DECRYPTED);
}
);
})
);

This is how the test file looks like

import test from 'ava';
import AWS from 'aws-sdk-mock';
import decrypt from './decrypt';
const ENCRYPTED = 'AOURNGRPGNARPGAR';
const DECRYPTED = 'decrypted-var-123';
test.beforeEach(() => {
AWS.mock('KMS', 'decrypt', { Plaintext : DECRYPTED });
});
test('Decrypting is returning mock value', async t => {
const result = await decrypt(ENCRYPTED);
t.is(result, DECRYPTED);
});

Mocking the functionality it’s very easy and the package currently support many functionalities from the original one.

I used .beforeEach to run the mock before each test in the file. (You can find more before & after hooks in the documentation).

What’s missing ?

End-to-end testing. We’re missing end-to-end testing. Even if we test our function locally, we know that’s not enough. My suggestion here is to test directly on AWS Lambda Dashboard or use tools like Serverless that does the job for you.

What I ❤️ about AVA

I’ll leave you with a small list about what I like about AVA, so if you haven’t tried it yet, give it a go!

  • Setup time is very, very small
  • Learning curve is super relaxed
  • Smart watch mode (if you save file b.test.js or b.js it will only run tests from b.js, not from other files)
  • test.only to run only that test, superb for development
  • ES6 and async/await syntax out of the box
  • nice string & object comparison
Thank You

--

--

Max Gallo
DAZN Engineering

Principal Engineer @ DAZN. Addicted to Technology, Design, Music, Motorbikes, Photography and Travels. I use spaces, not tabs.