Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core Integration Testing & Mocking using Moq

I have the following ASP.NET Core integration test using a custom 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;
    }
}

which looks like it should work but the problem is that the ConfigureTestServices method is never being called, so my mock is never registered with the IoC container. You can find the full source code 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();
}
like image 645
Muhammad Rehan Saeed Avatar asked Aug 20 '19 15:08

Muhammad Rehan Saeed


People also ask

What is integration testing in .NET Core?

Integration tests ensure that an app's components function correctly at a level that includes the app's supporting infrastructure, such as the database, file system, and network. ASP.NET Core supports integration tests using a unit test framework with a test web host and an in-memory test server.

Which testing framework is best for .NET Core?

xUnit test is the best Unit testing framework for . Net programming languages like C#, VB.Net, and F#. xUnit derives its structure and functionality from SUnit of Smalltalk.


3 Answers

I've blogged about ASP.NET Core Integration Testing & Mocking using Moq. It's not simple and requires some setup but I hope it helps someone out. Here is the basic code you need using ASP.NET Core 3.1:

Startup

The ConfigureServices and Configure methods in your applications Startup class must be virtual. This is so that we can iherit from this class in our tests and replace production versions of certain services with mock versions.

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) =>
        ...
}

TestStartup

In your test project, override the Startup class with one that registers the mock and the mock object with IoC.

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

In your test project, write a custom WebApplicationFactory that configures the HttpClient and resolves the mocks from the TestStartup, then exposes them as properties, ready for our integration test to consume them. Note that I'm also changing the environment to Testing and telling it to use the TestStartup class for startup.

Note also that I've implemented IDisposable's `Dispose method to verify all of my strict mocks. This means I don't need to verify any mocks manually myself. Verification of all mock setups happens automatically when xUnit is disposing the test 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);
    }
}

Integration Tests

I'm using xUnit to write my tests. Note that the generic type passed to CustomWebApplicationFactory is Startup and not TestStartup. This generic type is used to find the location of your application project on disk and not to start the application.

I setup a mock in my test and I've implemented IDisposable to verify all mocks for all my tests at the end but you can do this step in the test method itself if you like.

Note also, that I'm not using xUnit's IClassFixture to only boot up the application once as the ASP.NET Core documentation tells you to do. If I did so, I'd have to reset the mocks between each test and also you would only be able to run the integration tests serially one at a time. With the method below, each test is fully isolated and they can be run in parallel. This uses up more CPU and each test takes longer to execute but I think it's worth it.

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

I'm using xUnit. We need to turn off shadown copying, so any separate files like appsettings.json are placed in the right place beside the application DLL file. This ensures that our application running in an integration test can still read the appsettings.json file.

{
  "shadowCopy": false
}

appsettings.Testing.json

Should you have configuration that you want to change just for your integration tests, you can add a appsettings.Testing.json file into your application. This configuration file will only be read in our integration tests because we set the environment name to 'Testing'.

like image 197
Muhammad Rehan Saeed Avatar answered Sep 30 '22 05:09

Muhammad Rehan Saeed


The best way to handle this is to factor out parts of your Startup that will need to be substituted during test. For example, instead of calling services.AddDbContext<MyContext>(...); directly in ConfigureServices, create a virtual private method like:

private virtual void ConfigureDatabase(IServiceCollection services)
{
    services.AddDbContext<MyContext>(...);
}

Then, in your test project, create a class like TestStartup which derives from your SUT's Startup class. Then, you can override these virtual methods to sub in your test services, mocks, etc.

Finally, just do something like:

builder
    .UseEnvironment("Testing")
    .UseStartup<TestStartup>();
like image 42
Chris Pratt Avatar answered Sep 30 '22 05:09

Chris Pratt


You should create a fake startup:

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>();
    }
}

Then use it with IClassFixture<CustomWebApplicationFactory<FakeStartup>>.

Make sure to make your original ConfigureServices method virtual.

like image 28
Thanh Nguyen Avatar answered Sep 30 '22 04:09

Thanh Nguyen