Running automated tests locally

In this tutorial, you’ll learn how to run local automated tests for AWS Lambda functions.

Make sure to read Designing Testable Lambda Functions for tricks on minimising the need to run local integration tests as well.

Configuring access to AWS Resources

Lambda functions are simple JavaScript functions. This means that you can, in theory, just run everything locally as simple automated tests. In practice, Lambda code often talks to other services, in particularly on AWS, such as DynamoDB, S3 or SNS. If you want to run tests against those services as well, things get slightly more complicated. The key issue is how to handle authentication and authorisation for external systems. Here are some interesting guidelines to keep in mind:

Another critical thing to keep in mind, to avoid stupid mistakes and financial cost:

A good approach to authentication, that automates away a lot of potential problems, is to:

  1. create a separate AWS IAM account for testing, and assign the keys for that account to a separate profile in ~/.aws/credentials (Unix) or %USERPROFILE%\.aws\credentials (Windows).
  2. Configure IAM privileges for the test profile, so it can only access the test resources. Optionally, create an IAM role specifically for the profile, so you can add/remove privileges easily for various types of tests.
  3. In the test runner initialisation (so just once for the entire set of tests), tell AWS SDK to use the test profile, and pre-select the AWS region. For example:
AWS.config.credentials = new AWS.SharedIniFileCredentials({ profile: 'my_profile_name' });
AWS.config.update({region: 'us-west-1'});

Simulating Lambda events

You can just require your Lambda module directly from an automated test, and call the handler function as if you were testing a normal JavaScript function – because it is just a normal function. The Lambda execution environment normally takes care of operational management and invocation. It’s a common practice to create a mock Lambda context object, and then spy on done or succeed/fail depending on how you terminate the call execution.

Remember that the Context object contains properties that point to the currently executing function. A common trick for accessing non-AWS resources is to use those properties to detect the version and then load additional configuration files. If you have something similar in your function, remember to initialise the properties of the context object accordingly.

Simulating API Gateway requests

Important note: The content on this page is valid for Claudia API Builder 2.x. For 1.x versions, call the router function and expect an API Builder Request Object

If you’re using claudia-api-builder, require your API module and execute the proxyRouter function. This is the entry-point Lambda execution, and has the same arguments as normal Lambda handlers (so event and lambdaContext).

The event object

The event object coming into the handler function, during normal execution, is the API Gateway Proxy Object. In order to simulate the API builder routing, you’ll need to set the following properties:

You can fill in the other properties (eg queryStringParameters) according to what the test expects, for example:

var apiRequest = {
    requestContext: {
        resourcePath: '/echo',
        httpMethod: 'GET'
    },
    queryStringParameters: {
        name: 'mike'
    }
};

If you’re using generic path components, the path needs to be the generic definition, not the actual path, and the values should be in the pathParams property. For example:

var request = {
  requestContext: {
    httpMethod: 'HTTP',
    resourcePath: 'people/{id}/{action}'
  }, 
  pathParameters: {
    id: 1,
    action: 'delete'
  }
}

The mock Lambda context

The API Builder always terminates execution using the done method, so you can provide only that one in your test mock context. This is a typical Node.js callback, so when it gets called, the first argument will be an error or null, and the second argument will be the result in case there are no errors

An example

Here’s a Jasmine example using API Builder:

var underTest = require('../src/web-api.js');

describe('Facebook token verification', () => {
  var lambdaContextSpy;

  beforeEach(() => {
    lambdaContextSpy = jasmine.createSpyObj('lambdaContext', ['done']);
  });

  it('returns hub challenge if the tokens match', (done) => {
    underTest.proxyRouter({
      requestContext: {
        resourcePath: '/facebook',
        httpMethod: 'GET'
      },
      queryStringParameters: {
        'hub.verify_token': '12345',
        'hub.challenge': 'XHCG'
      },
      stageVariables: {
        facebookVerifyToken: '12345'
      }
    }, lambdaContextSpy).then(() => {
      expect(lambdaContextSpy.done).toHaveBeenCalledWith(null, jasmine.objectContaining({body:'XHCG'}));
    }).then(done, done.fail);
  });
});

Did you like this tutorial? Get notified when we publish the next one.

Once a month, high value mailing list, no ads or spam. (Check out the past issues)