ASP.NET 使用 Moq 进行核心集成测试和模拟
ASP.NET Core Integration Testing & Mocking using Moq
我有以下 ASP.NET Core integration test 使用自定义 WebApplicationFactory
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();
}
你应该创建一个虚假的启动:
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
方法设为虚拟方法。
处理此问题的最佳方法是排除 Startup
中需要在测试期间替换的部分。例如,不是直接在 ConfigureServices
中调用 services.AddDbContext<MyContext>(...);
,而是创建一个虚拟私有方法,如:
private virtual void ConfigureDatabase(IServiceCollection services)
{
services.AddDbContext<MyContext>(...);
}
然后,在您的测试项目中,创建一个类似于 TestStartup
的 class,它派生自您的 SUT 的 Startup
class。然后,您可以重写这些虚拟方法以在您的测试服务、模拟等中进行子
最后,只需执行以下操作:
builder
.UseEnvironment("Testing")
.UseStartup<TestStartup>();
我写了关于 ASP.NET Core Integration Testing & Mocking using Moq 的博客。这并不简单,需要一些设置,但我希望它能帮助别人。这是使用 ASP.NET Core 3.1 所需的基本代码:
启动
您的应用程序 Startup
class 中的 ConfigureServices
和 Configure
方法必须是虚拟的。这样我们就可以在我们的测试中继承这个 class 并将某些服务的生产版本替换为模拟版本。
public class Startup
{
private readonly IConfiguration configuration;
private readonly IWebHostingEnvironment webHostingEnvironment;
public Startup(IConfiguration configuration, IWebHostingEnvironment webHostingEnvironment)
{
this.configuration = configuration;
this.webHostingEnvironment = webHostingEnvironment;
}
public virtual void ConfigureServices(IServiceCollection services) =>
...
public virtual void Configure(IApplicationBuilder application) =>
...
}
测试启动
在您的测试项目中,用一个向 IoC 注册模拟和模拟对象的 Startup class 覆盖。
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);
}
}
CustomWebApplicationFactory
在您的测试项目中,编写自定义 WebApplicationFactory
配置 HttpClient
并解析来自 TestStartup
的模拟,然后将它们公开为属性,为我们的集成测试做好准备消耗它们。请注意,我还将环境更改为 Testing
并告诉它使用 TestStartup
class 进行启动。
另请注意,我已经实现了 IDisposable
的 `Dispose 方法来验证我所有的严格模拟。这意味着我不需要自己手动验证任何模拟。当 xUnit 处理测试时,所有模拟设置的验证会自动发生 class.
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 void ConfigureClient(HttpClient client)
{
using (var serviceScope = this.Services.CreateScope())
{
var serviceProvider = serviceScope.ServiceProvider;
this.ApplicationOptions = serviceProvider.GetRequiredService<IOptions<ApplicationOptions>>().Value;
this.ClockServiceMock = serviceProvider.GetRequiredService<Mock<IClockService>>();
}
base.ConfigureClient(client);
}
protected override void ConfigureWebHost(IWebHostBuilder builder) =>
builder
.UseEnvironment("Testing")
.UseStartup<TestStartup>();
protected override void Dispose(bool disposing)
{
if (disposing)
{
this.VerifyAllMocks();
}
base.Dispose(disposing);
}
}
集成测试
我正在使用 xUnit 编写我的测试。请注意,传递给 CustomWebApplicationFactory
的通用类型是 Startup
而不是 TestStartup
。此通用类型用于查找您的应用程序项目在磁盘上的位置,而不是用于启动应用程序。
我在我的测试中设置了一个模拟,并且我已经实施 IDisposable
以在最后验证我所有测试的所有模拟,但如果你愿意,你可以在测试方法本身中执行此步骤。
另请注意,我没有使用 xUnit 的 IClassFixture
只启动应用程序一次,正如 ASP.NET 核心文档告诉您的那样。如果我这样做,我将不得不在每次测试之间重置模拟,而且您一次只能 运行 连续进行一个集成测试。使用下面的方法,每个测试都是完全隔离的,它们可以 运行 并行。这会消耗更多 CPU 并且每个测试需要更长的时间来执行,但我认为这是值得的。
public class FooControllerTest : CustomWebApplicationFactory<Startup>
{
private readonly HttpClient client;
private readonly Mock<IClockService> clockServiceMock;
public FooControllerTest()
{
this.client = this.CreateClient();
this.clockServiceMock = this.ClockServiceMock;
}
[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);
}
}
xunit.runner.json
我正在使用 xUnit。我们需要关闭 shadown 复制,所以像 appsettings.json
这样的任何单独文件都放在应用程序 DLL 文件旁边的正确位置。这确保我们的应用程序 运行ning 在集成测试中仍然可以读取 appsettings.json
文件。
{
"shadowCopy": false
}
appsettings.Testing.json
如果您只想为集成测试更改配置,则可以将 appsettings.Testing.json
文件添加到您的应用程序中。这个配置文件只会在我们的集成测试中读取,因为我们将环境名称设置为 'Testing'.
我有以下 ASP.NET Core integration test 使用自定义 WebApplicationFactory
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();
}
你应该创建一个虚假的启动:
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
方法设为虚拟方法。
处理此问题的最佳方法是排除 Startup
中需要在测试期间替换的部分。例如,不是直接在 ConfigureServices
中调用 services.AddDbContext<MyContext>(...);
,而是创建一个虚拟私有方法,如:
private virtual void ConfigureDatabase(IServiceCollection services)
{
services.AddDbContext<MyContext>(...);
}
然后,在您的测试项目中,创建一个类似于 TestStartup
的 class,它派生自您的 SUT 的 Startup
class。然后,您可以重写这些虚拟方法以在您的测试服务、模拟等中进行子
最后,只需执行以下操作:
builder
.UseEnvironment("Testing")
.UseStartup<TestStartup>();
我写了关于 ASP.NET Core Integration Testing & Mocking using Moq 的博客。这并不简单,需要一些设置,但我希望它能帮助别人。这是使用 ASP.NET Core 3.1 所需的基本代码:
启动
您的应用程序 Startup
class 中的 ConfigureServices
和 Configure
方法必须是虚拟的。这样我们就可以在我们的测试中继承这个 class 并将某些服务的生产版本替换为模拟版本。
public class Startup
{
private readonly IConfiguration configuration;
private readonly IWebHostingEnvironment webHostingEnvironment;
public Startup(IConfiguration configuration, IWebHostingEnvironment webHostingEnvironment)
{
this.configuration = configuration;
this.webHostingEnvironment = webHostingEnvironment;
}
public virtual void ConfigureServices(IServiceCollection services) =>
...
public virtual void Configure(IApplicationBuilder application) =>
...
}
测试启动
在您的测试项目中,用一个向 IoC 注册模拟和模拟对象的 Startup class 覆盖。
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);
}
}
CustomWebApplicationFactory
在您的测试项目中,编写自定义 WebApplicationFactory
配置 HttpClient
并解析来自 TestStartup
的模拟,然后将它们公开为属性,为我们的集成测试做好准备消耗它们。请注意,我还将环境更改为 Testing
并告诉它使用 TestStartup
class 进行启动。
另请注意,我已经实现了 IDisposable
的 `Dispose 方法来验证我所有的严格模拟。这意味着我不需要自己手动验证任何模拟。当 xUnit 处理测试时,所有模拟设置的验证会自动发生 class.
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 void ConfigureClient(HttpClient client)
{
using (var serviceScope = this.Services.CreateScope())
{
var serviceProvider = serviceScope.ServiceProvider;
this.ApplicationOptions = serviceProvider.GetRequiredService<IOptions<ApplicationOptions>>().Value;
this.ClockServiceMock = serviceProvider.GetRequiredService<Mock<IClockService>>();
}
base.ConfigureClient(client);
}
protected override void ConfigureWebHost(IWebHostBuilder builder) =>
builder
.UseEnvironment("Testing")
.UseStartup<TestStartup>();
protected override void Dispose(bool disposing)
{
if (disposing)
{
this.VerifyAllMocks();
}
base.Dispose(disposing);
}
}
集成测试
我正在使用 xUnit 编写我的测试。请注意,传递给 CustomWebApplicationFactory
的通用类型是 Startup
而不是 TestStartup
。此通用类型用于查找您的应用程序项目在磁盘上的位置,而不是用于启动应用程序。
我在我的测试中设置了一个模拟,并且我已经实施 IDisposable
以在最后验证我所有测试的所有模拟,但如果你愿意,你可以在测试方法本身中执行此步骤。
另请注意,我没有使用 xUnit 的 IClassFixture
只启动应用程序一次,正如 ASP.NET 核心文档告诉您的那样。如果我这样做,我将不得不在每次测试之间重置模拟,而且您一次只能 运行 连续进行一个集成测试。使用下面的方法,每个测试都是完全隔离的,它们可以 运行 并行。这会消耗更多 CPU 并且每个测试需要更长的时间来执行,但我认为这是值得的。
public class FooControllerTest : CustomWebApplicationFactory<Startup>
{
private readonly HttpClient client;
private readonly Mock<IClockService> clockServiceMock;
public FooControllerTest()
{
this.client = this.CreateClient();
this.clockServiceMock = this.ClockServiceMock;
}
[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);
}
}
xunit.runner.json
我正在使用 xUnit。我们需要关闭 shadown 复制,所以像 appsettings.json
这样的任何单独文件都放在应用程序 DLL 文件旁边的正确位置。这确保我们的应用程序 运行ning 在集成测试中仍然可以读取 appsettings.json
文件。
{
"shadowCopy": false
}
appsettings.Testing.json
如果您只想为集成测试更改配置,则可以将 appsettings.Testing.json
文件添加到您的应用程序中。这个配置文件只会在我们的集成测试中读取,因为我们将环境名称设置为 'Testing'.