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));
}
}
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);