集成测试

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服务、消息队列等依赖可以使用测试替身或专门的测试容器。集成测试并不意味着所有外部系统都必须真实连接,关键是明确每次测试要验证的边界。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。