我使用自定义WebApplicationFactory
的以下ASP.NET Core integration test
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint>
where TEntryPoint : class
{
public CustomWebApplicationFactory()
{
this.ClientOptions.AllowAutoRedirect = false;
this.ClientOptions.BaseAddress = new Uri("https://localhost");
}
public ApplicationOptions ApplicationOptions { get; private set; }
public Mock<IClockService> ClockServiceMock { get; private set; }
public void VerifyAllMocks() => Mock.VerifyAll(this.ClockServiceMock);
protected override TestServer CreateServer(IWebHostBuilder builder)
{
this.ClockServiceMock = new Mock<IClockService>(MockBehavior.Strict);
builder
.UseEnvironment("Testing")
.ConfigureTestServices(
services =>
{
services.AddSingleton(this.ClockServiceMock.Object);
});
var testServer = base.CreateServer(builder);
using (var serviceScope = testServer.Host.Services.CreateScope())
{
var serviceProvider = serviceScope.ServiceProvider;
this.ApplicationOptions = serviceProvider.GetRequiredService<IOptions<ApplicationOptions>>().Value;
}
return testServer;
}
}
看起来应该可以工作,但是问题是ConfigureTestServices
方法从未被调用,所以我的模拟程序从未在IoC容器中注册。您可以找到完整的源代码here。
public class FooControllerTest : IClassFixture<CustomWebApplicationFactory<Startup>>, IDisposable
{
private readonly HttpClient client;
private readonly CustomWebApplicationFactory<Startup> factory;
private readonly Mock<IClockService> clockServiceMock;
public FooControllerTest(CustomWebApplicationFactory<Startup> factory)
{
this.factory = factory;
this.client = factory.CreateClient();
this.clockServiceMock = this.factory.ClockServiceMock;
}
[Fact]
public async Task Delete_FooFound_Returns204NoContent()
{
this.clockServiceMock.SetupGet(x => x.UtcNow).ReturnsAsync(new DateTimeOffset.UtcNow);
var response = await this.client.DeleteAsync("/foo/1");
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
public void Dispose() => this.factory.VerifyAllMocks();
}
答案 0 :(得分:2)
您应该创建一个伪造的启动公司:
public class FakeStartup : Startup
{
public FakeStartup(IConfiguration configuration)
: base(configuration)
{
}
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
// Your fake go here
//services.AddScoped<IService, FakeService>();
}
}
然后将其与IClassFixture<CustomWebApplicationFactory<FakeStartup>>
一起使用。
确保使用原始的ConfigureServices
虚拟方法。
答案 1 :(得分:1)
处理此问题的最佳方法是排除Startup
中需要在测试期间替换的部分。例如,与其直接在services.AddDbContext<MyContext>(...);
中调用ConfigureServices
,不如创建一个虚拟私有方法:
private virtual void ConfigureDatabase(IServiceCollection services)
{
services.AddDbContext<MyContext>(...);
}
然后,在测试项目中,创建一个类似TestStartup
的类,该类派生自您的SUT的Startup
类。然后,您可以覆盖这些虚拟方法以将其包含在测试服务,模拟等中。
最后,做类似的事情:
builder
.UseEnvironment("Testing")
.UseStartup<TestStartup>();
答案 2 :(得分:1)
Startup类中的ConfigureServices
和Configure
方法必须是可重写的。我从一个鲜为人知的Startup
基类继承StartupBase
来帮助实现这一点。
public class Startup : StartupBase
{
private readonly IConfiguration configuration;
private readonly IHostingEnvironment hostingEnvironment;
public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
{
this.configuration = configuration;
this.hostingEnvironment = hostingEnvironment;
}
public override void ConfigureServices(IServiceCollection services) =>
...
public override void Configure(IApplicationBuilder application) =>
...
}
在您的测试项目中,使用在IoC中注册模拟对象和模拟对象的类覆盖Startup类。
public class TestStartup : Startup
{
private readonly Mock<IClockService> clockServiceMock;
public TestStartup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
: base(configuration, hostingEnvironment)
{
this.clockServiceMock = new Mock<IClockService>(MockBehavior.Strict);
}
public override void ConfigureServices(IServiceCollection services)
{
services
.AddSingleton(this.clockServiceMock);
base.ConfigureServices(services);
services
.AddSingleton(this.clockServiceMock.Object);
}
}
在您的测试项目中,编写一个自定义WebApplicationFactory
,用于配置HttpClient
并从TestStartup
解析模拟,然后将其作为属性公开,以供我们的测试使用。请注意,我将环境更改为Testing
,并告诉它使用TestStartup
类进行启动。
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint>
where TEntryPoint : class
{
public CustomWebApplicationFactory()
{
this.ClientOptions.AllowAutoRedirect = false;
this.ClientOptions.BaseAddress = new Uri("https://localhost");
}
public ApplicationOptions ApplicationOptions { get; private set; }
public Mock<IClockService> ClockServiceMock { get; private set; }
public void VerifyAllMocks() => Mock.VerifyAll(this.ClockServiceMock);
protected override TestServer CreateServer(IWebHostBuilder builder)
{
builder
.UseEnvironment("Testing")
.UseStartup<TestStartup>();
var testServer = base.CreateServer(builder);
using (var serviceScope = testServer.Host.Services.CreateScope())
{
var serviceProvider = serviceScope.ServiceProvider;
this.ApplicationOptions = serviceProvider.GetRequiredService<IOptions<ApplicationOptions>>().Value;
this.ClockServiceMock = serviceProvider.GetRequiredService<Mock<IClockService>>();
}
return testServer;
}
}
我正在使用xUnit编写测试。请注意,传递给CustomWebApplicationFactory
的通用类型是Startup
而不是TestStartup
。此泛型类型用于在磁盘上查找应用程序项目的位置,而不用于启动应用程序。
我在测试中设置了一个模拟,并且我已经实现了IDisposable
来验证我所有测试的所有模拟,但是如果您愿意,可以在测试方法本身中执行此步骤。
public class FooControllerTest : IClassFixture<CustomWebApplicationFactory<Startup>>, IDisposable
{
private readonly HttpClient client;
private readonly CustomWebApplicationFactory<Startup> factory;
public FooControllerTest(CustomWebApplicationFactory<Startup> factory)
{
this.factory = factory;
this.client = factory.CreateClient();
}
[Fact]
public async Task GetFoo_Default_Returns200OK()
{
this.clockServiceMock.Setup(x => x.UtcNow).ReturnsAsync(new DateTimeOffset(2000, 1, 1));
var response = await this.client.GetAsync("/foo");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
public void Dispose() => this.factory.VerifyAllMocks();
}
我正在使用xUnit。我们需要关闭阴影复制,以便将appsettings.json
之类的单独文件放在正确的位置。我们还关闭了并行运行的测试,因为这可能导致状态在测试之间泄漏。在此示例中,我使用xUnit IClassFixture
,以便我们仅在一次测试类后就启动服务器,以节省时间。这是一个折衷方案,您可以启用并行运行测试并停止使用IClassFixture
,而只需在每个测试中增加一个新的WebApplicationFactory
。
{
"parallelizeTestCollections": false,
"shadowCopy": false
}