集成测试
ASP.NET Core应用的测试可以分为单元测试和集成测试。单元测试关注一个类或方法的行为,集成测试则可以启动真实的应用管道,验证路由、模型绑定、中间件、认证授权和数据库访问能否协同工作。这篇笔记介绍使用xUnit和WebApplicationFactory编写HTTP集成测试。
创建xUnit测试工程
在解决方案目录中创建测试工程,并添加对被测Web API工程的引用:
dotnet new xunit -n DemoWebAPI.Tests
dotnet add DemoWebAPI.Tests reference DemoWebAPI/DemoWebAPI.csproj
cd DemoWebAPI.Tests
dotnet add package Microsoft.AspNetCore.Mvc.Testing --version 6.0.36
具体命令中的项目路径按实际目录调整。Microsoft.AspNetCore.Mvc.Testing的版本应与被测应用的目标框架相匹配。xUnit模板已经包含测试SDK和xUnit基础依赖,可以使用以下命令运行测试:
dotnet test
xUnit中,[Fact]表示一个无参数测试,[Theory]可以配合[InlineData]编写多组输入的测试。测试方法可以是异步方法,并通过Assert验证结果。例如:
using Xunit;
public class AgePolicyTests
{
[Theory]
[InlineData(17, false)]
[InlineData(18, true)]
[InlineData(30, true)]
public void IsAdult_ReturnsExpectedResult(int age, bool expected)
{
bool result = age >= 18;
Assert.Equal(expected, result);
}
}
实际测试应调用被测业务方法,而不是把实现直接复制到测试中。这里仅为展示Theory的参数化形式。
让测试工程访问应用入口
使用顶级语句的Program.cs会生成一个默认不可被其他程序集访问的入口类。为了让WebApplicationFactory<Program>能够引用它,可以在Program.cs末尾添加:
public partial class Program
{
}
为了便于演示,假设应用中注册了如下端点:
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello, ASP.NET Core!" }));
使用WebApplicationFactory测试HTTP端点
WebApplicationFactory<TEntryPoint>可以在内存TestServer中启动应用,并创建用于调用该应用的HttpClient。测试不需要先手动运行Kestrel,也不依赖本机固定端口。
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
namespace DemoWebAPI.Tests;
public class HelloApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public HelloApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task GetHello_ReturnsSuccessAndMessage()
{
HttpClient client = _factory.CreateClient();
HttpResponseMessage response = await client.GetAsync("/hello");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<HelloResponse>();
Assert.Equal("Hello, ASP.NET Core!", body?.Message);
}
private sealed record HelloResponse(string Message);
}
IClassFixture让同一个测试类中的测试共享工厂。若测试会修改共享数据库、缓存或其他状态,应注意测试隔离,不能简单假设所有测试都可以安全并行执行。
替换测试配置和服务
WithWebHostBuilder()可以定制测试宿主。例如将业务服务替换为测试实现:
using DemoWebAPI.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
WebApplicationFactory<Program> factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
services.RemoveAll<IStudentService>();
services.AddSingleton<IStudentService, FakeStudentService>();
});
});
HttpClient client = factory.CreateClient();
ConfigureServices中的修改发生在应用的服务注册之后,因此可以替换已有注册。但如果某项配置在Program.cs构建应用之前就已经被读取并用于其他注册,还需要通过测试配置、环境变量或专门的测试入口进行覆盖,不能只替换最终服务。
测试数据库常见方案包括:
- 使用独立测试数据库,并在每个测试前清理和准备数据,行为最接近真实数据库。
- 使用SQLite内存数据库测试关系映射和查询,需要保持连接开放,并注意与生产数据库提供程序的差异。
- 使用EF Core InMemory Provider做有限的测试,但它不是关系数据库,不能验证真实SQL、约束、事务和提供程序特定行为。
测试完成后应释放工厂和客户端;需要重复定制宿主时,可以继承WebApplicationFactory<Program>封装统一的测试配置。
测试认证授权和中间件
集成测试不仅应验证成功路径,还应覆盖:
- 未认证请求是否返回401,已认证但无权限是否返回403。
- 模型验证失败是否返回400和约定的Problem Details。
- 全局异常处理是否隐藏内部堆栈,同时记录可关联的日志。
- CORS、防伪、响应缓存等中间件是否按预期工作。
测试认证时,可以注册一个仅在Testing环境使用的测试认证处理器,按请求头或测试数据构造ClaimsPrincipal,不要为了方便测试而在生产配置中关闭授权。
单元测试与集成测试的边界
计算规则、验证逻辑等适合使用单元测试;依赖真实路由、DI、序列化和中间件的行为适合使用WebApplicationFactory集成测试。外部HTTP服务、消息队列等依赖可以使用测试替身或专门的测试容器。集成测试并不意味着所有外部系统都必须真实连接,关键是明确每次测试要验证的边界。