如何在AWS Lambda单元测试中Mock IDynamoDBContext并重构代码
如何Mock IDynamoDBContext实现Lambda函数的单元测试
问题背景
已有基于AWS Serverless的API,可对DynamoDB表执行CRUD操作,功能正常,但在为CI/CD流水线添加单元测试时遇到问题:测试因无法创建DynamoDB上下文而失败,且不想将其作为集成测试。当前使用Nunit和NSubstitute进行测试,现有代码无法注入IDynamoDBContext,导致Mock方案无法适用。
重构步骤
1. 修改Lambda函数,实现依赖注入
原来的函数在内部硬编码创建DynamoDBContext,无法替换为Mock对象。我们需要通过构造函数注入IDynamoDBContext,同时保留无参构造函数供Lambda运行时使用(Lambda要求函数必须有无参构造)。
修改后的ReadFunction代码:
using System.Text.Json; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DataModel; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; using Tenants.Domain.Entities; using System.Collections.Generic; [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] namespace Tenants.Read; public class ReadFunction { private readonly IDynamoDBContext _dbContext; // 供单元测试使用的构造函数,注入Mock的IDynamoDBContext public ReadFunction(IDynamoDBContext dbContext) { _dbContext = dbContext; } // 供Lambda运行时使用的无参构造函数,创建真实的DynamoDBContext public ReadFunction() { var config = new AmazonDynamoDBConfig { AuthenticationRegion = "us-east-1" }; var client = new AmazonDynamoDBClient(config); _dbContext = new DynamoDBContext(client); } public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) { string response; int statusCode = 200; try { // 直接使用注入的_dbContext,不再内部创建 var tenants = await _dbContext.ScanAsync<Tenant>(new List<ScanCondition>()).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" } } }; } }
2. 编写单元测试,Mock IDynamoDBContext和IAsyncSearch
ScanAsync方法返回的是IAsyncSearch<Tenant>接口,所以需要Mock这个接口来模拟GetRemainingAsync的返回结果。使用NSubstitute可以轻松实现这一点。
修改后的单元测试代码:
using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; using Amazon.DynamoDBv2.DataModel; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.TestUtilities; using NSubstitute; using NUnit.Framework; using Tenants.Domain.Entities; namespace Tenants.Read.Tests; [TestFixture] public class ReadFunctionTest { [Test] public async Task FunctionHandler_Returns200_WithTenants() { // Arrange // 预设测试用的Tenant列表 var mockTenants = new List<Tenant> { new Tenant { Id = "1", Name = "Test Tenant 1" }, new Tenant { Id = "2", Name = "Test Tenant 2" } }; // Mock IAsyncSearch<Tenant>,模拟GetRemainingAsync返回预设列表 var mockAsyncSearch = Substitute.For<IAsyncSearch<Tenant>>(); mockAsyncSearch.GetRemainingAsync().Returns(mockTenants); // Mock IDynamoDBContext,模拟ScanAsync返回Mock的IAsyncSearch var mockDbContext = Substitute.For<IDynamoDBContext>(); mockDbContext.ScanAsync<Tenant>(Arg.Any<List<ScanCondition>>()).Returns(mockAsyncSearch); // 使用带参数的构造函数注入Mock对象 var function = new ReadFunction(mockDbContext); var context = new TestLambdaContext(); var request = new APIGatewayProxyRequest(); // Act var response = await function.FunctionHandler(request, context); var returnedTenants = JsonSerializer.Deserialize<List<Tenant>>(response.Body); // Assert Assert.That(response.StatusCode, Is.EqualTo(200)); Assert.That(returnedTenants?.Count, Is.EqualTo(2)); Assert.That(returnedTenants?[0].Name, Is.EqualTo("Test Tenant 1")); } [Test] public async Task FunctionHandler_Returns500_OnException() { // Arrange var mockAsyncSearch = Substitute.For<IAsyncSearch<Tenant>>(); mockAsyncSearch.GetRemainingAsync().Throws(new Exception("Database error")); var mockDbContext = Substitute.For<IDynamoDBContext>(); mockDbContext.ScanAsync<Tenant>(Arg.Any<List<ScanCondition>>()).Returns(mockAsyncSearch); var function = new ReadFunction(mockDbContext); var context = new TestLambdaContext(); var request = new APIGatewayProxyRequest(); // Act var response = await function.FunctionHandler(request, context); var errorMessage = JsonSerializer.Deserialize<string>(response.Body); // Assert Assert.That(response.StatusCode, Is.EqualTo(500)); Assert.That(errorMessage, Is.EqualTo("Database error")); } }
关键说明
- 依赖注入改造:通过添加带参数的构造函数,让单元测试可以注入Mock的
IDynamoDBContext,而Lambda运行时使用无参构造创建真实上下文,兼顾生产环境和测试需求。 - Mock IAsyncSearch:因为
ScanAsync返回的是IAsyncSearch<T>,必须Mock这个接口的GetRemainingAsync方法,才能模拟数据库查询的返回结果或异常。
内容的提问来源于stack exchange,提问作者John Barnes
相关产品推荐
相关产品推荐

