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

时间:2019-08-20 15:32:30

标签: asp.net-core asp.net-core-mvc moq xunit asp.net-core-testhost

我使用自定义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();
}

3 个答案:

答案 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类中的ConfigureServicesConfigure方法必须是可重写的。我从一个鲜为人知的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) =>
        ...
}

TestStartup

在您的测试项目中,使用在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类进行启动。

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.runner.json

我正在使用xUnit。我们需要关闭阴影复制,以便将appsettings.json之类的单独文件放在正确的位置。我们还关闭了并行运行的测试,因为这可能导致状态在测试之间泄漏。在此示例中,我使用xUnit IClassFixture,以便我们仅在一次测试类后就启动服务器,以节省时间。这是一个折衷方案,您可以启用并行运行测试并停止使用IClassFixture,而只需在每个测试中增加一个新的WebApplicationFactory

{
  "parallelizeTestCollections": false,
  "shadowCopy": false
}