使用 Moq 进行 ASP.NET Core 集成测试和模拟

Muh*_*eed 10 moq xunit asp.net-core-mvc asp.net-core asp.net-core-testhost

我有以下使用自定义的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;
    }
}
Run Code Online (Sandbox Code Playgroud)

看起来它应该可以工作,但问题是该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();
}
Run Code Online (Sandbox Code Playgroud)

Muh*_*eed 14

我在博客上写过关于使用 Moq 的 ASP.NET Core 集成测试和模拟。这并不简单,需要一些设置,但我希望它可以帮助某人。以下是使用 ASP.NET Core 3.1 所需的基本代码:

启动

应用程序类中的ConfigureServicesConfigure方法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) =>
        ...
}
Run Code Online (Sandbox Code Playgroud)

测试启动

在您的测试项目中,使用一个将模拟和模拟对象注册到 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

自定义Web应用程序工厂

在您的测试项目中,编写一个自定义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);
    }
}
Run Code Online (Sandbox Code Playgroud)

集成测试

我正在使用 xUnit 来编写我的测试。请注意,传递给的泛型类型CustomWebApplicationFactoryisStartup和 not TestStartup。此通用类型用于在磁盘上查找应用程序项目的位置,而不是启动应用程序。

我在我的测试中设置了一个模拟,并且我已经实现了IDisposable在最后验证我所有测试的所有模拟,但是如果你愿意,你可以在测试方法本身中执行这一步。

另请注意,我没有IClassFixture像 ASP.NET Core 文档告诉您那样使用 xUnit来仅启动应用程序一次。如果我这样做,我将不得不在每个测试之间重置模拟,而且您一次只能连续运行一个集成测试。使用下面的方法,每个测试都是完全隔离的,它们可以并行运行。这会消耗更多的 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

xunit.runner.json

我正在使用 xUnit。我们需要关闭 shadown 复制,因此任何单独的文件appsettings.json都被放置在应用程序 DLL 文件旁边的正确位置。这确保了我们在集成测试中运行的应用程序仍然可以读取appsettings.json文件。

{
  "shadowCopy": false
}
Run Code Online (Sandbox Code Playgroud)

appsettings.Testing.json

如果您只想为集成测试更改配置,则可以将appsettings.Testing.json文件添加到应用程序中。这个配置文件只会在我们的集成测试中被读取,因为我们将环境名称设置为“Testing”。

  • 你说“我没有像 ASP.NET Core 文档告诉你的那样,使用 xUnit 的 IClassFixture 只启动应用程序一次。如果我这样做,我就必须在每个测试之间重置模拟,而且你也只会能够一次连续运行一个集成测试。” 我理解 IClassFixture 是共享东西...但是共享的东西是一个工厂,不应该有状态,所以并行运行是安全的吗?显然不是,因为我似乎遇到了竞争条件错误......愿意解释共享工厂如何具有可能破坏测试的状态吗? (2认同)