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:
Find the copyable code snippets, here
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.
Find the copyable code snippets, here
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.
Find the copyable code snippets, here
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:
Find the copyable code snippets, here
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?