Asp.net-Core

使用 Moq 的 ASP.NET Core 集成測試和模擬

  • December 30, 2020

我有以下使用自定義的ASP.NET Core 集成測試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 容器中註冊。您可以在此處找到完整的原始碼。

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``Startup

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 類。

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了最後驗證我所有測試的所有模擬,但如果你願意,你可以在測試方法本身中執行此步驟。

另請注意,我沒有使用 xUnitIClassFixture來僅啟動應用程序一次,因為 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文件添加到應用程序中。此配置文件將僅在我們的集成測試中讀取,因為我們將環境名稱設置為“測試”。

引用自:https://stackoverflow.com/questions/57577045