AWS Open Source Blog
Testing AWS Lambda functions written in Java
Testing is an essential task when building software. Testing helps improve software quality by finding bugs before they reach production. The sooner we know there is a defect in code, the easier and cheaper it is to correct. Automated tests are a central piece in reducing this feedback loop. In association with a continuous integration and continuous deployment (CI/CD) pipeline, tests should reduce the number of issues discovered in production. Also, testing provides some confidence when updating an application, a sort of seat belt that will reduce the risk of introducing regressions.
AWS Lambda is a serverless compute service that lets you run code in response to events. Lambda natively supports several runtimes: Node.js, Python, Ruby, Go, PowerShell, C#, and Java. With Lambda, you can bring code and whatever libraries you need to perform any operation, be it a REST API backend, a scheduled operation, or any other logic that suits your business. And like any other piece of code, a Lambda function deserves to be tested.
We know that writing unit and integration tests for Lambda can be a challenge, especially in Java, and today we are happy to announce the release of aws-lambda-java-tests, an opinionated library that simplifies writing tests.
Anatomy of a Java Lambda function handler
The handler is the entry point of your Lambda function: This is the method that is executed when the Lambda function is invoked. It receives the event that triggered the invocation as a parameter along with the context. The aws-lambda-java-core library defines two interfaces to help you write this handler. Let’s focus on the RequestHandler
:
When you use this interface, the Java runtime deserializes the event into an object with the input type (I
) and serializes the the result with output type (O
) into text.
As an example, the following function takes a Map
(key/value) as an event and return a simple String
.
If this function receives the following event (in JSON format), it will provide a Map
to your handler and will return the message value (“Hello from Lambda!”
):
Using advanced events
The previous example is quite simple, and most of the time you will need more complex things, such as an Amazon Simple Queue Service (Amazon SQS) or an Amazon API Gateway event. This is where the aws-lambda-java-events library comes into play. This library provides event definitions for most of the events Lambda natively supports.
For example, ScheduledEvent
represents a Amazon Cloudwatch Event or Amazon EventBridge event:
And here is an example of JSON event that can be deserialized with the ScheduleEvent
class:
Let’s test
Why am I telling you all this and how does this relate to tests? Because when you test your Lambda function (EventHandler
in the following examples), you want to inject some JSON events (input), and validate that it behaves as expected and returns the correct response (output).
You can imagine loading a JSON file from your test resources, deserializing it with a JSON library such as Jackson or Gson, and then invoking your handler with this object:
Easy, isn’t it? Yes, until you discover that “detail-type” is not correctly deserialized in the detailType
attribute. And the same problem will happen with Amazon SQS, Amazon Kinesis, Amazon DynamoDB “Records” (vs. “records” in the code), and many others. Indeed, the Lambda Java Runtime is not just a simple Jackson ObjectMapper; the (de)serialization process takes care of these “specificities.”
Unfortunately, you don’t have access to this runtime for your tests. Or at least that was true until when we, during re:Invent, announced the support of container images to package your Lambda function. If you carefully read the announcement, you will see:
We have open-sourced a set of software packages, Runtime Interface Clients (RIC), that implement the Lambda Runtime API, allowing you to seamlessly extend your preferred base images to be Lambda compatible. The Lambda Runtime Interface Clients are available for popular programming language runtimes.
And Java was not forgotten! Looking at the aws-lambda-java-libs repository, you will see the appearance of several new libraries: aws-lambda-java-runtime-interface-client, and another, more discreet but just as useful, aws-lambda-java-serialization.
If you look at this last one, this is exactly what we need to correctly deserialize the special events. Taking the ScheduledEvent
as an example, we can see there is a SceduledEventMixin
class:
Note: Mix-ins are a powerful Jackson feature that allows to specify how to (de)serialize attributes in classes you cannot modify—in a third-party library, for example. And this library defines all the mix-ins you need (SQS, SNS, Kinesis, and so on). Now we can rewrite our previous test:
Thanks to this library, we are now able to inject JSON events in our tests and get them correctly deserialized, but it remains as verbose as before.
Introducing aws-lambda-java-tests
This new library provides tools to seamlessly load and deserialize JSON events and inject them in your tests.
Setup
To use it, add the following dependency to your project. Note that it’s a test dependency.
Also have surefire in your plugins:
Load events
With the help of the library, we can now simplify our previous test:
EventLoader
provides loaders for the most common event types available in aws-lambda-java-events—Amazon Simple Notification Service (Amazon SNS), Amazon SQS, Amazon Kinesis, Amazon Simple Storage Service (Amazon S3), Amazon DynamoDB, and many others. We also can load our own event types:
JSON files should be in the classpath, generally in src/test/resources
folder.
That’s a great first step but we can go further.
Inject events in tests
A set of annotations can be used to inject events and/or validate handler responses against those events. All these annotations must be used in conjunction with the @ParameterizedTest
annotation from JUnit 5.
ParameterizedTest
enables to inject arguments into a test, so we can run the same test one or more times with different parameters.
With the @Event
annotation, we can rewrite our previous test in that way:
Ideally, we would like to test multiple events. The @Events
annotation allows this:
And ultimately, we would like to verify that when we inject a specific event (or set of events), we receive a specific response (or set of responses). The @HandlerParams
annotation provides this feature:
In this test, all events available in the apigw/events/
folder and all responses in the folder apigw/responses
will be injected in the event and response parameters of the test method. So with one unique simple test, we are able to validate many use cases and thus significantly reduce the amount of code to write. To get it working correctly, we should have the same number of files in each folder and correctly ordered so that a response matches an event.
If at some point, we discover a bug in a Lambda function, we can retrieve the JSON event in the CloudWatch logs and copy it into the events folder. Create the expected JSON response, put it in the response folder, and we’re done. The next time we will push the function code, the CI/CD pipeline will execute this test and ensures no regression is introduced.
Conclusion
Testing Lambda functions is as important as any other piece of software, it will provide you more confidence when deploying to production and reduce the number of issues you will meet at this level. Thanks to this new library, testing functions written in Java becomes easier. With a few annotations and lines of code, you can validate the different behaviours of your function according to the events it receives.
Find the full documentation and source code for aws-lambda-java-tests on GitHub. Feel free to contribute via pull requests and also to raise an issue if you have feedback or discover a problem.
Learn more about AWS Lambda testing in our prescriptive test guidance, and find additional java based test examples on GitHub. For more serverless learning resources, visit Serverless Land.