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

单元测试APIGatewayProxyFunction控制器方法:C#继承与模拟依赖问题

解决方案:在AWS Lambda+ASP.NET Core Web API测试中替换依赖并验证路由

针对你的需求,这里提供三种可行方案,既能验证MVC路由、HTTP方法匹配,又能注入NSubstitute模拟的数据库仓储,且优先避免启动完整HTTP服务:


方案1:自定义测试用LambdaEntryPoint,覆写Host构建逻辑

核心思路是继承自动生成的LambdaEntryPoint,通过覆写CreateHostBuilder方法替换DI服务,避开构造函数链中Init()调用导致的模拟实例null问题。

代码示例

首先定义仓储接口:

public interface IProductRepository
{
    Task<Product> GetByIdAsync(int id);
}

创建测试专用的Lambda入口类:

public class TestLambdaEntryPoint : APIGatewayProxyFunction
{
    // 暴露模拟仓储属性,供测试类注入
    public IProductRepository MockRepository { get; set; }

    protected override IHostBuilder CreateHostBuilder()
    {
        return Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>()
                    .ConfigureServices(services =>
                    {
                        // 替换真实仓储为模拟实例
                        if (MockRepository != null)
                        {
                            services.Replace(ServiceDescriptor.Scoped<IProductRepository>(_ => MockRepository));
                        }
                    });
            });
    }
}

编写单元测试:

[Fact]
public async Task GetProduct_ValidId_ReturnsOk()
{
    // 1. 创建并配置模拟仓储
    var mockRepo = Substitute.For<IProductRepository>();
    mockRepo.GetByIdAsync(1).Returns(new Product { Id = 1, Name = "Test Product" });

    // 2. 初始化测试用Lambda入口,注入模拟仓储
    var lambdaEntry = new TestLambdaEntryPoint
    {
        MockRepository = mockRepo
    };
    await lambdaEntry.InitAsync();

    // 3. 构造API Gateway格式的请求
    var request = new APIGatewayProxyRequest
    {
        HttpMethod = "GET",
        Path = "/api/products/1",
        Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
    };

    // 4. 执行请求并验证结果
    var response = await lambdaEntry.FunctionHandlerAsync(request, new LambdaContext());

    Assert.Equal(200, int.Parse(response.StatusCode));
    var product = JsonSerializer.Deserialize<Product>(response.Body);
    Assert.Equal(1, product.Id);
    // 验证模拟仓储方法被正确调用
    await mockRepo.Received(1).GetByIdAsync(1);
}

方案2:使用WebApplicationFactory(官方内存测试工具)

利用ASP.NET Core官方的WebApplicationFactory,在内存中模拟请求处理,无需启动真实HTTP服务,同时轻松替换DI依赖。

代码示例

创建自定义工厂类:

public class ProductApiFactory : WebApplicationFactory<Startup>
{
    public IProductRepository MockRepository { get; set; }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        base.ConfigureWebHost(builder);
        builder.ConfigureServices(services =>
        {
            // 移除真实仓储的注册
            var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IProductRepository));
            if (descriptor != null)
            {
                services.Remove(descriptor);
            }
            // 注入模拟仓储
            if (MockRepository != null)
            {
                services.AddScoped<IProductRepository>(_ => MockRepository);
            }
        });
    }

    // 适配API Gateway请求格式到HttpContext
    public async Task<APIGatewayProxyResponse> SendGatewayRequestAsync(APIGatewayProxyRequest request)
    {
        var client = CreateClient();
        var httpRequest = new HttpRequestMessage(new HttpMethod(request.HttpMethod), request.Path);
        
        // 复制请求头
        foreach (var header in request.Headers)
        {
            if (!httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value))
            {
                httpRequest.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value);
            }
        }

        // 处理请求体
        if (!string.IsNullOrEmpty(request.Body))
        {
            httpRequest.Content = new StringContent(request.Body, Encoding.UTF8, "application/json");
        }

        var httpResponse = await client.SendAsync(httpRequest);
        
        // 转换回API Gateway响应格式
        return new APIGatewayProxyResponse
        {
            StatusCode = (int)httpResponse.StatusCode,
            Body = await httpResponse.Content.ReadAsStringAsync(),
            Headers = httpResponse.Headers.ToDictionary(h => h.Key, h => h.Value.FirstOrDefault())
        };
    }
}

编写单元测试:

[Fact]
public async Task GetProduct_ValidId_ReturnsOk()
{
    var mockRepo = Substitute.For<IProductRepository>();
    mockRepo.GetByIdAsync(1).Returns(new Product { Id = 1, Name = "Test Product" });

    using var factory = new ProductApiFactory
    {
        MockRepository = mockRepo
    };

    var request = new APIGatewayProxyRequest
    {
        HttpMethod = "GET",
        Path = "/api/products/1"
    };

    var response = await factory.SendGatewayRequestAsync(request);

    Assert.Equal(200, int.Parse(response.StatusCode));
    var product = JsonSerializer.Deserialize<Product>(response.Body);
    Assert.Equal(1, product.Id);
    await mockRepo.Received(1).GetByIdAsync(1);
}

方案3:直接构建Host并手动执行中间件管道

如果需要高度自定义请求场景,可以直接构建Host,手动创建HttpContext并执行中间件管道,完全在内存中处理请求。

代码示例

[Fact]
public async Task PostProduct_ValidData_ReturnsCreated()
{
    var mockRepo = Substitute.For<IProductRepository>();
    mockRepo.CreateAsync(Arg.Any<Product>()).Returns((Product p) => p);

    // 构建Host并替换依赖
    var host = Host.CreateDefaultBuilder()
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>()
                .ConfigureServices(services =>
                {
                    services.Replace(ServiceDescriptor.Scoped<IProductRepository>(_ => mockRepo));
                });
        })
        .Build();

    await host.StartAsync();

    try
    {
        // 获取必要服务
        var httpContextFactory = host.Services.GetRequiredService<IHttpContextFactory>();
        var endpointRoutingMiddleware = host.Services.GetRequiredService<EndpointRoutingMiddleware>();
        var endpointMiddleware = host.Services.GetRequiredService<EndpointMiddleware>();

        // 创建模拟HttpContext
        var httpContext = httpContextFactory.Create();
        httpContext.Request.Method = "POST";
        httpContext.Request.Path = "/api/products";
        httpContext.Request.ContentType = "application/json";
        
        // 写入请求体
        var productToCreate = new Product { Name = "New Product" };
        var json = JsonSerializer.Serialize(productToCreate);
        await using var writer = new StreamWriter(httpContext.Request.Body);
        await writer.WriteAsync(json);
        await writer.FlushAsync();
        httpContext.Request.Body.Position = 0;

        // 执行中间件管道
        await endpointRoutingMiddleware.Invoke(httpContext);
        await endpointMiddleware.Invoke(httpContext);

        // 验证结果
        Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode);
        var responseBody = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
        var createdProduct = JsonSerializer.Deserialize<Product>(responseBody);
        Assert.Equal("New Product", createdProduct.Name);
        await mockRepo.Received(1).CreateAsync(Arg.Is<Product>(p => p.Name == "New Product"));
    }
    finally
    {
        await host.StopAsync();
        await host.DisposeAsync();
    }
}

方案选择建议

  • 优先选方案1:最贴近Lambda真实运行环境,能完整验证API Gateway与Lambda的交互逻辑。
  • 若需要更通用的ASP.NET Core测试能力,选方案2:WebApplicationFactory是官方推荐的测试工具,生态完善。
  • 方案3适合需要高度自定义请求细节的场景,灵活性最高但代码量稍大。

内容的提问来源于stack exchange,提问作者AceGambit

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 01:50:32