.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实现整个测试类的测试资源共享:
- 先定义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(); } }
- 测试类实现
IClassFixture<IntegrationTestFixture>,通过构造函数注入共享资源:
public sealed class IntegrationTests : IClassFixture<IntegrationTestFixture> { private HttpClient Client { get; } public IntegrationTests(IntegrationTestFixture fixture) { Client = fixture.Client; } // 你的测试用例 }
内容的提问来源于stack exchange,提问作者Ian Horwill
相关产品推荐
相关产品推荐

