Testing is a crucial step in the software development lifecycle, enhancing quality by detecting bugs early. Automated tests and CI/CD pipelines streamline this process, reducing production issues and instilling confidence during updates to prevent regressions.
Just as traditional software development, your Lambda function testing strategy should:
Serverless applications are typically built using your function code and a set of cloud-based managed services, such as queues, databases, event buses, and messaging systems.
If in traditional software development, typically you can run it locally including most, if not all, its dependencies. Replicating an entire cloud environment, including queues, database tables, event buses, security policies is not practical. You will inevitably encounter issues due to differences between your local environment and your deployed environments in the cloud.
There are three main techniques to test Serverless solutions:
Testing in the cloud - you deploy infrastructure and code to test with actual services, security policies, configurations and infrastructure-specific parameters. Cloud-based tests provide the most accurate measure of the code quality.
Testing with mocks - Mocks are objects within your code that simulate and stand-in for an external service. Mocks provide pre-defined behavior to verify service calls and parameters. Mocks can mimic and simplify complex dependencies, but can also lead to more mocks in order to replace nested dependencies.
Testing with emulators - You can set up applications (sometimes from a third party) to mimic a cloud service in your local environment. Speed is their strength, but setup and parity with production services are their weakness.
In the following sections we will implement Unit Test, Integration Test and End-To-End Test with the techniques mentioned above using a fully functional Java Serverless application.
Let’s consider a ticketing serverless application which provides an API endpoint to persist tickets in a DynamoDb Table. When the API endpoint is called, the Lambda function deserializes the userId and ticket description from the HTTP request and calls the putItem DynamoDB API to persist the item.
The source code of this example is available at serverless-test-samples
The handler is the entry point of a 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 the RequestHandler interface as follows:
public interface RequestHandler<I, O> {
O handleRequest(I input, Context context);
}
When the Lambda Function class implements the RequestHandler interface,
the Java runtime deserializes the event into an object with the input type
I
and serializes the response using the output type O
.
The TicketFunction
is invoked from API Gateway, therefore, it needs to deserialize and API Gateway request
using the APIGatewayProxyRequestEvent
and to serialize the response with APIGatewayProxyResponseEvent
.
These classes are defined in the aws-lambda-java-events
library
which provides event definitions for most of the events natively supported by Lambda.
public class TicketFunction implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) {
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
String ticketId;
try {
Ticket ticket = mapper.readValue(event.getBody(), Ticket.class);
logger.info("[ticket userId] " + ticket.getUserId());
logger.info("[ticket description] " + ticket.getDescription());
ticketId = ddbUtils.persistTicket(ticket);
response.setBody(mapper.writeValueAsString(ticketId));
} catch (JsonProcessingException e) {
logger.error("Error creating new ticket ", e);
response.setStatusCode(HttpStatusCode.BAD_REQUEST);
} catch (Exception e) {
logger.error("Error creating new ticket ", e);
response.setStatusCode(HttpStatusCode.INTERNAL_SERVER_ERROR);
}
return response;
}
}