You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 5使用TestHost.TestServer做Web API集成测试时如何替换服务实现

解决方案

你遇到的配置顺序问题是WebHostBuilder的默认设计导致的:直接调用ConfigureServices传入的委托默认会在Startup.ConfigureServices执行之前运行,和UseStartup的调用顺序无关,所以你注册的测试服务会被后续Startup里注册的生产服务覆盖,可通过以下几种方式解决:

方法1:使用带runAfterStartup参数的ConfigureServices重载

这是对现有代码改动最小的方案,调用ConfigureServices时指定第二个参数runAfterStartup: true,就能让你的服务注册逻辑在Startup的服务注册完成之后执行,这时候你注册的测试服务属于后注册的实现,会自动覆盖生产实现,也可以正常执行移除旧实现的逻辑。
修改后的代码如下:

public sealed class IntegrationTests : IDisposable
{
    private TestServer Server { get; }
    private HttpClient Client { get; }

    public IntegrationTests()
    {
        var builder = new WebHostBuilder()
            .UseConfiguration(new ConfigurationBuilder().AddJsonFile("appsettings.development.json").Build())
            .UseSerilog()
            .UseStartup<Startup>()
            // 新增第二个参数指定在Startup之后执行
            .ConfigureServices(UseDummyUserRepository, runAfterStartup: true);

        Server = new TestServer(builder);
        Client = Server.CreateClient();
    }

    private void UseDummyUserRepository(IServiceCollection services)
    {
        // 此时Startup的注册逻辑已经执行完成,可以正常移除原有实现
        services.Remove(services.First(s => s.ServiceType == typeof(IUserRepository)));
        services.AddTransient<IUserRepository, DummyUserRepository>();
    }

    public void Dispose()
    {
        Client.Dispose();
        Server.Dispose();
    }
}

方法2:使用ConfigureTestServices扩展方法

如果你已经引用了Microsoft.AspNetCore.Mvc.Testing NuGet包(WebApplicationFactory所在的包),可以直接用ConfigureTestServices方法,这个方法原生就是设计用来注册测试服务的,默认就会在Startup的服务注册逻辑之后执行:

var builder = new WebHostBuilder()
    .UseConfiguration(new ConfigurationBuilder().AddJsonFile("appsettings.development.json").Build())
    .UseSerilog()
    .UseStartup<Startup>()
    // 替换为ConfigureTestServices方法即可
    .ConfigureTestServices(UseDummyUserRepository);

方法3:自定义测试Startup类

如果不想依赖上面的特性,可以自己写一个测试用的Startup类继承项目原生的Startup,重写服务注册逻辑:

// 测试项目内定义
public class TestStartup : Startup
{
    public TestStartup(IConfiguration configuration) : base(configuration) { }

    public override void ConfigureServices(IServiceCollection services)
    {
        // 先执行原生的服务注册逻辑
        base.ConfigureServices(services);
        // 再替换为测试实现
        services.Remove(services.First(s => s.ServiceType == typeof(IUserRepository)));
        services.AddTransient<IUserRepository, DummyUserRepository>();
    }
}

构造TestServer的时候指定UseStartup<TestStartup>()即可。

XUnit IClassFixture用法补充

你当前的写法会在每个测试用例执行时都新建一次TestServer和HttpClient,测试较多时会很慢,推荐用IClassFixture实现整个测试类的测试资源共享:

  1. 先定义Fixture类存放共享资源:
public class IntegrationTestFixture : IDisposable
{
    public TestServer Server { get; }
    public HttpClient Client { get; }

    public IntegrationTestFixture()
    {
        // 把原来的TestServer初始化逻辑放到这里
        var builder = new WebHostBuilder()
            .UseConfiguration(new ConfigurationBuilder().AddJsonFile("appsettings.development.json").Build())
            .UseSerilog()
            .UseStartup<Startup>()
            .ConfigureServices(UseDummyUserRepository, runAfterStartup: true);

        Server = new TestServer(builder);
        Client = Server.CreateClient();
    }

    private void UseDummyUserRepository(IServiceCollection services)
    {
        services.Remove(services.First(s => s.ServiceType == typeof(IUserRepository)));
        services.AddTransient<IUserRepository, DummyUserRepository>();
    }

    public void Dispose()
    {
        Client.Dispose();
        Server.Dispose();
    }
}
  1. 测试类实现IClassFixture<IntegrationTestFixture>,通过构造函数注入共享资源:
public sealed class IntegrationTests : IClassFixture<IntegrationTestFixture>
{
    private HttpClient Client { get; }

    public IntegrationTests(IntegrationTestFixture fixture)
    {
        Client = fixture.Client;
    }

    // 你的测试用例
}

内容的提问来源于stack exchange,提问作者Ian Horwill

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 16:06:04