使用 Moq 的 ASP.NET Core 集成測試和模擬
我有以下使用自定義的ASP.NET Core 集成測試
WebApplicationFactorypublic 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 容器中註冊。您可以在此處找到完整的原始碼。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(); }
我寫過關於使用 Moq 進行 ASP.NET Core 集成測試和模擬的部落格。這並不簡單,需要一些設置,但我希望它可以幫助某人。以下是使用 ASP.NET Core 3.1 所需的基本程式碼:
啟動
應用程序類中的
ConfigureServicesand方法必須是虛擬的。這樣我們就可以在我們的測試中繼承這個類,並用模擬版本替換某些服務的生產版本。Configure``Startuppublic 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 類。
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該類進行啟動。另請注意,我已經實現
IDisposable了 `Dispose 方法來驗證我所有的嚴格模擬。這意味著我不需要自己手動驗證任何模擬。當 xUnit 處理測試類時,會自動驗證所有模擬設置。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 來編寫我的測試。請注意,傳遞給
CustomWebApplicationFactoryisStartup和 not的泛型類型TestStartup。此泛型類型用於查找應用程序項目在磁碟上的位置,而不是啟動應用程序。我在我的測試中設置了一個模擬,並且我已經實現
IDisposable了最後驗證我所有測試的所有模擬,但如果你願意,你可以在測試方法本身中執行此步驟。另請注意,我沒有使用 xUnit
IClassFixture來僅啟動應用程序一次,因為 ASP.NET Core 文件告訴您這樣做。如果我這樣做了,我將不得不在每個測試之間重置模擬,而且您一次只能連續執行一個集成測試。使用下面的方法,每個測試都是完全隔離的,它們可以並行執行。這會佔用更多的 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 文件旁邊的正確位置。這確保了我們在集成測試中執行的應用程序仍然可以讀取該appsettings.json文件。{ "shadowCopy": false }appsettings.Testing.json
如果您想為集成測試更改配置,您可以將
appsettings.Testing.json文件添加到應用程序中。此配置文件將僅在我們的集成測試中讀取,因為我們將環境名稱設置為“測試”。