最小 API 应用中的单元和集成测试

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

作者:Fiyaz Bin HasanRick Anderson

集成测试简介

单元测试相比,集成测试可在更广泛的级别上评估应用的组件。 单元测试用于测试独立软件组件,如单独的类方法。 集成测试确认两个或更多应用组件一起工作以生成预期结果,可能包括完整处理请求所需的每个组件。

这些更广泛的测试用于测试应用的基础结构和整个框架,通常包括以下组件:

  • 数据库
  • 文件系统
  • 网络设备
  • 请求-响应管道

单元测试使用称为 fake 或 mock 对象的制造组件,而不是基础结构组件。

与单元测试相比,集成测试:

  • 使用应用在生产环境中使用的实际组件。
  • 需要进行更多代码和数据处理。
  • 需要更长时间来运行。

因此,将集成测试的使用限制为最重要的基础结构方案。 如果可以使用单元测试或集成测试来测试行为,请选择单元测试。

在集成测试的讨论中,测试的项目经常称为“测试中的系统”,简称“SUT”。 本文中使用“SUT”来指代要测试的 ASP.NET Core 应用。

请勿为通过数据库和文件系统进行的数据和文件访问的每个排列编写集成测试。 无论应用中有多少位置与数据库和文件系统交互,一组集中式读取、写入、更新和删除集成测试通常能够充分测试数据库和文件系统组件。 将单元测试用于与这些组件交互的方法逻辑的例程测试。 在单元测试中,使用基础结构 fake 或 mock 会导致更快地执行测试。

ASP.NET Core 集成测试

ASP.NET Core 中的集成测试需要以下内容:

  • 测试项目用于包含和执行测试。 测试项目具有对 SUT 的引用。
  • 测试项目为 SUT 创建测试 Web 主机,并使用测试服务器客户端处理 SUT 的请求和响应。
  • 测试运行程序用于执行测试并报告测试结果。

集成测试后跟一系列事件,包括常规“排列”、“操作”和“断言”测试步骤:

  1. 已配置 SUT 的 Web 主机。
  2. 创建测试服务器客户端以向应用提交请求。
  3. 执行“排列”测试步骤:测试应用会准备请求。
  4. 执行“操作”测试步骤:客户端提交请求并接收响应。
  5. 执行“断言”测试步骤:实际响应基于预期响应验证为通过或失败。
  6. 该过程会一直继续,直到执行了所有测试。
  7. 报告测试结果。

通常,测试 Web 主机的配置与用于测试运行的应用常规 Web 主机不同。 例如,可以将不同的数据库或不同的应用设置用于测试。

基础结构组件(如测试 Web 主机和内存中测试服务器 (TestServer))由 Microsoft.AspNetCore.Mvc.Testing 包提供或管理。 使用此包可简化测试创建和执行。

Microsoft.AspNetCore.Mvc.Testing 包处理以下任务:

  • 将依赖项文件 (.deps) 从 SUT 复制到测试项目的 bin 目录中。
  • 内容根目录设置为 SUT 的项目根目录,以便可在执行测试时找到静态文件和页面/视图。
  • 提供 WebApplicationFactory 类,以简化 SUT 在 TestServer 中的启动过程。

单元测试文档介绍如何设置测试项目和测试运行程序,以及有关如何运行测试的详细说明与有关如何命名测试和测试类的建议。

将单元测试与集成测试分隔到不同的项目中。 分隔测试:

  • 有助于确保不会意外地将基础结构测试组件包含在单元测试中。
  • 允许控制运行的测试集。

GitHub 上的示例代码提供最小 API 应用上的单元和集成测试示例。

IResult 实现类型

使用命名方法而不是 Lambda 时,可以使用 Microsoft.AspNetCore.Http.HttpResults 命名空间中的公共 IResult 实现类型对最小路由处理程序进行单元测试。

下面的代码使用 NotFound<TValue> 类:

[Fact]
public async Task GetTodoReturnsNotFoundIfNotExists()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var notFoundResult = (NotFound) result.Result;

    Assert.NotNull(notFoundResult);
}

下面的代码使用 Ok<TValue> 类:

[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title",
        Description = "Test description",
        IsDone = false
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var okResult = (Ok<Todo>)result.Result;

    Assert.NotNull(okResult.Value);
    Assert.Equal(1, okResult.Value.Id);
}

其他资源