单元测试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
相关产品推荐
相关产品推荐

