Search code examples
c#aws-lambdaamazon-dynamodbnunitaws-serverless

Mocking DynamoDB in an AWS Lambda Unit Test


I have an AWS Serverless API that performs CRUD operations on a DynamoDB table. The functions work as expected, but I am having trouble trying to add unit tests to support our CI/CD pipeline. The tests fail because they can't create the DynamoDB context and I don't want them to as they would be integration tests. How can I refactor the function so that I can mock IDynamoDBContext and/or write a test for this function?

Note: I am using Nunit and NSubstitute for testing. I'm open to other testing libraries if it makes this easier. I have seen examples of mocking IDynamoDBContext and subclassing AsyncSearch, but I can't inject IDynamoDBContext so that will not work with the code as written.

A simplified version of my Read Lambda function currently looks like this:

using System.Text.Json;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Tenants.Domain.Entities;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace Tenants.Read;

public class ReadFunction
{

    public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
    {
        string response;
        int statusCode = 200;

        try
        {
            DynamoDBContext dbContext = CreateDynamoDbContext();
            List<Tenant> tenants = await dbContext.ScanAsync<Tenant>([]).GetRemainingAsync();

            response = JsonSerializer.Serialize(tenants);
        }
        catch (Exception ex)
        {
            statusCode = 500;
            response = JsonSerializer.Serialize(ex.Message);
        }

        return new APIGatewayProxyResponse
        {
            Body = response,
            StatusCode = statusCode,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }

    private static DynamoDBContext CreateDynamoDbContext()
    {
        AmazonDynamoDBConfig config = new() { AuthenticationRegion = "us-east-1" };
        AmazonDynamoDBClient client = new(config);

        return new DynamoDBContext(client);
    }
}

The beginning of my unit test looks like this:

using System.Text.Json;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.TestUtilities;
using NUnit.Framework;

namespace Tenants.Read.Tests;

[TestFixture]
public class ReadFunctionTest
{
    [Test]
    public async Task FunctionHandler_Returns200()
    {
        // Arrange
        var function = new ReadFunction();
        var context = new TestLambdaContext();
        var request = new APIGatewayProxyRequest();

        // Act
        APIGatewayProxyResponse response = await function.FunctionHandler(request, context);

        // Assert
        Assert.That(response.StatusCode, Is.EqualTo(200));
    }
}

Solution

  • You are able to add a second parameterised Function constructor with Lambda and perform your DI there for testing.

    Have a look at this article from AWS showing a DB dependency (under heading "Testing tools"): https://docs.aws.amazon.com/lambda/latest/dg/dotnet-csharp-testing.html

    In your scenario, it would include something like:

    private readonly IDynamoDBContext _dbContext
    
    public ReadFunction(IDynamoDBContext dbContext)
    {
        _dbContext = dbContext;
    }
    

    Then you can mock this with:

    [TestFixture]
    public class ReadFunctionTest
    {
        [Test]
        public async Task FunctionHandler_Returns200()
        {
            var mockDynamoDBContext = new DynamoDBContext(Substitute.For<IAmazonDynamoDB>());
            var context = new TestLambdaContext();
            var function = new ReadFunction(mockDynamoDBContext);