实战指南:使用 xUnit 和 ASP.NET Core 进行集成测试【完整教程】

引言

集成测试可在包含应用支持基础结构(如数据库、文件系统和网络)的级别上确保应用组件功能正常。 ASP.NET Core 通过将单元测试框架与测试 Web 主机和内存中测试服务器结合使用来支持集成测试。

简介

集成测试与单元测试相比,能够在更广泛的级别上评估应用的组件,确认多个组件一起工作以生成预期结果,包括数据库、文件系统、网络设备等组件。单元测试主要用于测试独立软件组件,如类方法,通常使用 fakemock 对象。集成测试使用实际组件,需要更多代码和数据处理,运行时间更长。建议将集成测试限制在重要的基础结构方案上,若可用单元测试或集成测试测试行为,优先选择单元测试。集成测试中被测试的项目通常称为"SUT",用于指代要测试的应用。避免为每种数据库和文件系统交互排列编写集成测试,而是通过一组集中式读取、写入、更新和删除集成测试充分测试这些组件,使用单元测试测试与这些组件交互的方法逻辑,使用 fakemock 对象可加快测试速度。

集成测试实战

我们在之前的章节中创建了Sample.ApiSample.Repository的项目,现在我们对这个项目进行整体的集成测试,带大家来感受一下。

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

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

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

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

上面解释到了集成测试中被测试的项目通常称为SUT。我们要测试的项目Sample.Api既是我们的SUT

好了我们开始创建xUnit的单元测试项目,并添加Sample.Api的项目引用.

集成测试第一步

在我们的单元测试项目中安装Nuget依赖

package 复制代码
PM> NuGet\Install-Package Microsoft.AspNetCore.Mvc.Testing -Version 8.0.4

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

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

将依赖项文件 (.deps) 从 SUT 复制到测试项目的 bin目录中。

将内容根目录设置为 SUT 的项目根目录,以便可在执行测试时找到静态文件和页面/视图。

提供 WebApplicationFactory 类,以简化 SUTTestServer 中的启动过程。

概念有点多,后续里面的概念会慢慢讲到。

我们知道Asp.Net CoreWeb项目项目是借助Kestrel启动,用集成测试的TestServer正是代替了Kestrel托管服务的启动,那我们要测试的项目就不需要单独被启动了。

什么是TestServer?

TestServer 用于在集成测试中模拟和启动应用程序的主机环境。通过创建 TestServer 实例,可以在测试中模拟出一个运行中的应用程序实例,以便进行端到端的集成测试。

Microsoft.AspNetCore.Mvc.Testing中已经默认集成了对TestServer的支持所以,不需要额外进行配置。

SUT 环境?

如果未设置 SUT 的环境,则环境会默认为开发环境即 Development

向测试项目公开启动类Program

使用 WebApplicationFactory<TEntryPoint> 创建 TestServer以进行集成测试。 TEntryPointSUT 的入口点类,通常是 Program.cs

有两种向测试项目公开 Program 的方法

  • Program.cs添加部分类
c# 复制代码
  var builder = WebApplication.CreateBuilder(args);
  // ... Configure services, routes, etc.
  app.Run();
  + public partial class Program { }
  • 配置MSBuild
    SUTcsproj文件下添加
xml 复制代码
	<ItemGroup>
		<Using Include="Sample.Api" />
		<InternalsVisibleTo Include="dotNetParadise.IntegrationTest" />
	</ItemGroup>
  1. <Using Include="Sample.Api" />:这个子元素指定了要在项目中使用的命名空间或程序集。在这里,Sample.Api 被包含在项目中,以便项目可以访问和使用该命名空间或程序集中的内容。

  2. <InternalsVisibleTo Include="dotNetParadise.IntegrationTest" />:这个子元素用于将内部可见性(InternalsVisibleTo)属性应用于项目,允许指定的程序集(在这里是 dotNetParadise.IntegrationTest)访问项目中的内部成员。这在单元测试或集成测试中非常有用,因为测试项目通常需要访问被测试项目的内部成员以进行更全面的测试。

相对来说更推荐使用第一种部分类的形式来对测试项目公开。

WebApplicationFactory

可以使用默认的WebApplicationFactory和自定义的WebApplicationFactory来进行集成测试

测试类实现一个类固定例程接口 (IClassFixture),以指示类包含测试,并在类中的所有测试间提供共享对象实例。

来感受一下

使用默认 WebApplicationFactory 的基本测试

看一下如何使用

c# 复制代码
public class DefaultWebApplicationFactoryTest : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public DefaultWebApplicationFactoryTest(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task GetAll_Query_ReturnOkAndListStaff()
    {
        //Arrange
        var httpClient = _factory.CreateClient();
        //act
        var response = await httpClient.GetAsync("/api/Staff");
        //Assert
        //校验状态码
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        //校验用户
        var users = await response.Content.ReadFromJsonAsync<List<Staff>>();
        Assert.NotNull(users);
    }

    [Fact]
    public async Task GetConfig_WhenCalled_ReturnOk() {
        //Arrange
        var httpClient = _factory.CreateClient();
        //act
        var response = await httpClient.GetAsync("/GetConfig");
        //Assert
        //校验状态码
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        //校验用户
        var config = await response.Content.ReadFromJsonAsync<string>();
        Assert.NotNull(config);
    }
}

看到我们的测试类继承了IClassFixture来共享实例对象,并且泛型类型是默认的WebApplicationFactory<Program>

接下来在我们的SUTSample.Apiprogram中打个断点来验证一下

看到了我们测试时默认的 WebApplicationFactory 使用默认配置启动应用程序主机,包括加载 appsettings.json 等配置文件。

在我们的appsettings.Development.json中加了一个配置

json 复制代码
{
  "Config": "这里是appsettings.Development.json"
}

GetConfig_WhenCalled_ReturnOk 测试方法看下结果

正确的读到appsettings.Development.json的内容了,从而可以得出我们上面的结论,如果未设置 SUT 的环境,则环境会默认为开发环境即 Development

从上面我们看到我们向SUT发请求是调用的CreateClient()
CreateClient() 方法用于创建一个 HttpClient 实例,用于模拟客户端与 SUT 进行交互。通过这个 HttpClient,测试代码可以发送 HTTP 请求到应用程序,并验证应用程序的响应。

总的来说默认的 WebApplicationFactory 提供了一种快速启动应用程序主机进行集成测试的方式,适用于简单的测试场景。

自定义 WebApplicationFactory

通过从 WebApplicationFactory<TEntryPoint> 来创建一个或多个自定义工厂,可以独立于测试类创建 Web 主机配置

我们来创建一个SampleApiWebAppFactory的类,然后继承WebApplicationFactory<Program>

c# 复制代码
public class SampleApiWebAppFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {

        builder.ConfigureServices((context, services) =>
        {

        });
        builder.UseEnvironment("Production");
        base.ConfigureWebHost(builder);
    }

    public HttpClient Client()
    {
        return CreateDefaultClient();
    }
}

里面有Asp.Net Core 启动项配置,我们都可以在自定义的SampleApiWebAppFactory进行重写, 自定义的 WebApplicationFactory 提供了一种灵活的方式来定制化应用程序主机的配置,并扩展功能以满足特定的测试需求。通过继承并重写 ConfigureWebHost 方法等,开发人员可以对应用程序主机进行自定义配置,包括添加新的服务、中间件或修改默认配置,从而在测试环境中模拟特定的场景或功能。

优势和功能扩展:

  • 定制化配置: 自定义的 WebApplicationFactory 允许开发人员根据测试需求添加自定义配置,比如测试环境特定的服务、中间件或其他设置,以确保测试环境与实际生产环境保持一致或满足特定测试需求。
  • 功能扩展: 通过重写 ConfigureWebHost 方法,开发人员可以扩展应用程序主机的功能,例如注册额外的服务、修改中间件管道、添加测试专用的配置等,从而更好地适应测试场景。

复杂性和维护:

  • 定制化代码量增加: 自定义的 WebApplicationFactory 可能会包含更多的定制化代码,需要更多的理解和维护,但这样可以更好地控制应用程序主机的配置和功能。
  • 更高的灵活性: 虽然需要更多的理解和维护,但自定义的 WebApplicationFactory 提供了更大的灵活性和定制性,可以满足更复杂的测试需求,并确保测试环境的准确性和一致性。

总的来说,通过自定义的 WebApplicationFactory,开发人员可以根据具体的测试场景和需求定制化配置和功能,以确保在集成测试中能够模拟真实的应用程序环境,并进行更全面和准确的测试。这种方式允许开发人员更好地控制应用程序主机的设置,以适应不同的测试需求和场景。

SUT 的数据库上下文在 Program.cs 中注册。 测试应用的 builder.ConfigureServices 回调在执行应用的 Program.cs 代码之后执行。 若要将与应用数据库不同的数据库用于测试,必须在 builder.ConfigureServices 中替换应用的数据库上下文。

c# 复制代码
builder.ConfigureServices((context, services) =>
{
    var descriptor = new ServiceDescriptor(
        typeof(DbContextOptions<SampleDbContext>),
        serviceProvider => DbContextFactory<SampleDbContext>(serviceProvider, (sp, o) =>
        {
            o.UseInMemoryDatabase("TestDB");
        }),
         ServiceLifetime.Scoped);

    services.Replace(descriptor);
});

上面用到的DbContextFactory方法

c# 复制代码
    private static DbContextOptions<TContext> DbContextFactory<TContext>(IServiceProvider applicationServiceProvider,
      Action<IServiceProvider, DbContextOptionsBuilder> optionsAction)
      where TContext : DbContext
    {
        var builder = new DbContextOptionsBuilder<TContext>(
            new DbContextOptions<TContext>(new Dictionary<Type, IDbContextOptionsExtension>()));

        builder.UseApplicationServiceProvider(applicationServiceProvider);

        optionsAction?.Invoke(applicationServiceProvider, builder);

        return builder.Options;
    }

来写个集成测试

c# 复制代码
public class SampleApiTest(SampleApiWebAppFactory factory) : IClassFixture<SampleApiWebAppFactory>
{

    [Fact]
    public async Task GetAll_Query_ReturnOkAndListStaff()
    {
        //Arrange
        var httpClient = factory.CreateClient();
        //act
        var response = await httpClient.GetAsync("/api/Staff");
        //Assert
        //校验状态码
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        //校验用户
        var users = await response.Content.ReadFromJsonAsync<List<Staff>>();
        Assert.NotNull(users);
    }

    [Fact]
    public async Task GetConfig_WhenCalled_ReturnOk()
    {
        //Arrange
        var httpClient = factory.CreateClient();
        //act
        var response = await httpClient.GetAsync("/GetConfig");
        //Assert
        //校验状态码
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        //校验用户
        var config = await response.Content.ReadFromJsonAsync<string>();
        Assert.NotNull(config);
    }

    // 后面测试省略。。。。
}

最后

集成测试是确保应用组件在包含数据库、文件系统和网络等基础结构的级别上正常运行的重要方式。ASP.NET Core通过结合单元测试框架、测试Web主机和内存中测试服务器来支持集成测试。

在集成测试中,我们评估应用组件在更广泛的级别上的功能,验证多个组件一起工作以生成预期结果,包括数据库、文件系统、网络设备等。单元测试主要用于测试独立的软件组件,而集成测试需要使用实际组件,涉及更多的代码和数据处理,以及更长的运行时间。建议将集成测试限制在重要的基础结构方案上,优先选择单元测试或集成测试来测试行为。

在集成测试中,被测试的项目通常称为SUTSystem Under Test),用于指代要测试的应用。避免为每种数据库和文件系统交互编写独立的集成测试,而是通过一组集中式的测试来全面测试这些组件,并使用单元测试来测试与这些组件交互的方法逻辑。

通过自定义的WebApplicationFactory,可以根据测试需求定制化配置和功能,模拟真实的应用程序环境进行全面和准确的测试。自定义的WebApplicationFactory提供了灵活性和定制性,满足复杂的测试需求,并确保测试环境的准确性。虽然自定义的WebApplicationFactory可能需要更多的理解和维护,但能更好地适应不同的测试场景。

集成测试是确保应用程序正常运行的关键步骤,通过综合不同组件的功能来验证应用的整体表现,提高应用程序的质量和稳定性。