You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 19:53:14