使用 xunit DependencyInjection 对 Minimal API 进行测试

Intro

ASP.NET Core 从 .NET 6 开始支持了 Minimal API, 对于 Minimal API 的的集成测试,之前一直并没有太多的关注,之前看到了这个 issue

https://github.com/pengweiqhca/Xunit.DependencyInjection/issues/86,大体上想使用 xunit DependencyInjection 来实现对 Minimal API 的集成测试,起初使用 WebApplicationFactory 写了一个示例,但是后来发现并不是人家想要的,提 issue 的同学希望在 test case 中的依赖注入和实际 web 是一个 DI,前两天后来研究了一下 WebApplicationFactory 是如何实现支持 Minimal API 的,最后在 xunit dependency injection 中提供了支持.

Implement

WebApplicationFactory 及相关的一些逻辑的实现源码可以参考最后的参考链接部分,大概总结一下

我们在 build host 的时候会产生两个诊断事件,分别是 HostBuilding 和 HostBuilt

  • HostBuilding 事件会在准备开始 build host 的时候触发
  • HostBuilt 事件会在 host 构建好之后触发

通过监听这两个事件在 HostBuilding 的时候获取到 IHostBuilder 并加入一些 host 注册配置逻辑,再通过 HostBuilt 事件拿到最后构建好的 IHost

具体的实现细节可以参考最后参考链接的源码

因为用到的 DeferredHostBuilder 以及 HostFactoryResolver 都是 internal 的目前实现是通过反射来执行的,因为只会在 test 执行的时候执行一次,觉得问题不大,感兴趣的可以去看源码:https://github.com/pengweiqhca/Xunit.DependencyInjection/blob/main/src/Xunit.DependencyInjection.AspNetCoreTesting/MinimalApiHostBuilderFactory.cs

Sample

使用示例如下:

Minimal API Sample

using MinimalApiSample;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IRandomService, RandomService>();
var app = builder.Build();
app.Map("/", () => "Hello MinimalAPI");
app.Run();

public partial class Program {}

这里为了体现是一个 DI,注入了一个示例服务,实现如下:

namespace MinimalApiSample;

public interface IRandomService
{
    int GetNumber(int max = 100);
}

public sealed class RandomService : IRandomService
{
    public int GetNumber(int max = 100) => Random.Shared.Next(max);
}

Test Startup

然后来看我们的测试代码,测试 Startup 的配置如下:

using Microsoft.Extensions.Hosting;
using Xunit.DependencyInjection.AspNetCoreTesting;

namespace MinimalApiTest;

public sealed class Startup
{
    public IHostBuilder CreateHostBuilder() => 
        MinimalApiHostBuilderFactory.GetHostBuilder<Program>();
}

使用 MinimalApiHostBuilderFactory.GetHostBuilder<Program>() 来创建一个自定义的 HostBuilder

测试项目需要引用 Xunit.DependencyInjection.AspNetCoreTesting nuget package

Test cases

最后看下我们的测试用例:

using MinimalApiSample;
using Xunit;

namespace MinimalApiTest;

public class ApiTest
{
    private readonly HttpClient _testClient;
    private readonly IRandomService _randomService;

    public ApiTest(HttpClient testClient, IRandomService randomService)
    {
        _testClient = testClient;
        _randomService = randomService;
    }

    [Fact]
    public async Task HelloTest()
    {
        var responseText = await _testClient.GetStringAsync("/");
        Assert.Equal("Hello MinimalAPI", responseText);
    }

    [Fact]
    public void ServiceTest()
    {
        var num = _randomService.GetNumber();
        Assert.True(num < 100);
    }
}

跑一下测试用例看下输出结果

使用 xunit DependencyInjection 对 Minimal API 进行测试

More

里面的实现感觉很巧妙很精巧,有兴趣的童鞋可以研究下源码看下