3 min read

Testing Serverless Functions Locally

I've been really enjoying playing with the Serverless framework recently. The FaaS (Function-as-a-Service) model really appeals to me, and I think it's going to be A Big Deal in the future. The Serverless framework makes interacting with AWS Lambda, API GateWay, and CloudFormation really really easy.

Unfortunately, because your code runs on other-people's-computers (aka. The Cloud) the development workflow is still in its early days. As a developer you spend a lot of time waiting for your function to be packaged (with any dependencies it required), and pushed across the network. Waiting is lame, so I wanted to investigate what I could use to speed up the development process.

Comparing Packages

While there's a number of libraries for simulating AWS Lambda locally, I narrowed down the ones I would try with the fantastic NPM Compare tool I only recently discovered. It makes it super quick and easy to look at the high-level metrics of NPM packages (e.g. stars on Github, frequency of updates, number of maintainers, etc); While these metrics aren't the be-all and end-all, they're a good indication of the "health" of a library.

After a bit of research, I decided to give the top two contenders a run: node-lambda and aws-lambda-local
Here's their comparison on NPM Compare for reference.

node-lambda

This package has been out for a couple of years, which given AWS Lambda was introduced in 2014 is impressive - that combined with the number of releases suggests a lot of effort has gone in to it.

I found the interface to node-lamba a bit unintuitive; If you call the base command with -h you get a stack trace - it only works properly on the commands like run, etc. Calling commands like run without command line options also results in stack traces. Fortunately the project docs are quite decent, but I do like a tool that takes the time to help you use it rather than just taking a dump on your terminal...
The arguments expected by the node-lambda run command were also strange; Why is specifying the event.json done with -j and the context.json with -x? Surely -e and -c are more intuitive? The long arguments even start with those letters, and they're not taken by other commands (--eventFile and --contextFile respectively). Strange.

Once I'd worked out the correct commands and arguments to use, it worked as expected. I like that specifying the handler function matched what's used in serverless.yml. Here's an example command based on my Serverless directory structure (which I will elaborate on later):

$ node-lambda run -H index.handler -j event.json -x context.json

node-lambda also has commands related to packaging and deploying Lambda functions, but these weren't really relevant because I have Serverless to take care of that for me.

aws-lambda-local

One of the big selling points of this package is its lack of dependencies. Given a Lambda Function is just another JavaScript function called with a few special parameters, I like the idea of a simple solution.

Here's the same (nested) function call:

$ lambda-local  -f index.js -c context.json -e event.json

One of the things I liked about aws-lambda-local was that the event and context arguments are optional. In the case of the context.json file/object, if you didn't specify it an automatically generated context object was created for you (with sensible fake data). Oh, and the arguments made much more sense to me...

Unfortunately the tool expects only the handler function to be exported from the function file (i.e. it assumes the first exported function it finds is your handler, and there's no way to configure this). This meant my practice of exporting my functions (so they can be tested) didn't play nicely. To get it working I had to make sure I exported my handler function first in all my files. Not a big deal, but not something I would've changed otherwise.

Conclusion

For now I'll go with aws-lambda-local because it's lightweight and doesn't duplicate a bunch of Serverless functionality.

I can run my local test from npm with scripts in my package.json like this one (assuming aws-lambda-local is installed globally):

{
...
  "scripts": {
    "install:invoke": "cd functions/install && lambda-local -f index.js -e event.json -c context.json"
  },
...
}