Integration Testing with ASP.NET Core 3.1 - Remove the Boiler Plate

Writing integration tests for your application while keeping your tests as DRY as possible by removing the boiler plate code

Published on 16 December 2019

Now we have an integration test which runs against our application and swaps out the 3rd party dependency we are going to want to create a number of different tests to check various items and scenarios. Looking at the code you can see this will get pretty repeative very quickly and this in itself is a code smell. Remember that unit and integration test code should be looked after as much as production application code. This means if a code smell is starting to appear then refactoring of the tests is the next step.

Original Integration Test

Let's take a look at our current setup and see how we can change it.

[Fact]
public async Task Test3()
{
    // Arrange
    var hostBuilder = new HostBuilder()
        .ConfigureWebHost(webHost =>
        {
            // Add TestServer
            webHost.UseTestServer();
            webHost.UseStartup<WebApplication41.Startup>();

            // configure the services after the startup has been called.
            webHost.ConfigureTestServices(services =>
            {
                // register the test one specifically
                services.SwapTransient<IMySimpleGithubClient, MockSimpleGithubClient>();
            });

        });

    // Build and start the IHost
    var host = await hostBuilder.StartAsync();

    // Create an HttpClient to send requests to the TestServer
    var client = host.GetTestClient();

    // Act
    var response = await client.GetAsync("/");
    
    // Assert
    var responseString = await response.Content.ReadAsStringAsync();
    
    var item = System.Text.Json.JsonSerializer.Deserialize<GithubUser>(responseString);
    item.Name.Should().Be("My Test User");
}

As we can see the "Arrange" section of the test is getting quite cumbersome to manage. So how can we solve this?

As part of Microsoft.AspNetCore.Mvc.Testing it provides a WebApplicationFactory which can be used for this.

What is a WebApplicationFactory<>?

The documentation site describes it perfectly - https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1?view=aspnetcore-3.0

Factory for bootstrapping an application in memory for functional end to end tests.

But what does this actually mean?

It allows for doing all the setup which we were manually doing with the HostBuilder ourselves before.

Using the factory the Test endpoint test now becomes ...

[Fact]
public async Task BasicEndPointTest()
{
    // Arrange
    var factory = new WebApplicationFactory<WebApplication41.Startup>();
    
    // Create an HttpClient which is setup for the test host
    var client = factory.CreateClient();

    // Act
    var response = await client.GetAsync("/Home/Test");

    // Assert
    var responseString = await response.Content.ReadAsStringAsync();
    responseString.Should().Be("This is a test");
}

As you can see from above this reduces the boiler plate code dramatically. Happy days!

However some of the eagle eyed of you will have noticed that we are using the Startup class directly from our application but not swapping out the dependency. This is the next step.

Using the WebApplicationFactory

Looking through the api surface of the WebApplicationFactory there is a way to swap out dependencies as before but it feels a bit clunky.

Let me explain.

var masterFactory = new WebApplicationFactory<WebApplication41.Startup>();
var factory = masterFactory.WithWebHostBuilder(builder =>
{
    builder.ConfigureTestServices(
        services =>
            {
                services.SwapTransient<IMySimpleGithubClient, MockSimpleGithubClient>();
            });
});

First off you create a master factory for the application. Then using that you create a further factory with the specified swap. You have to access the IWebHostBuilder and then the ConfigureTestServices method on that. This isn't too much different from what we originally but it works.

Extend the WebAppplicationFactory

Now I'm all about making life easier for developers. One of the most common excuses I've heard over the years why people don't write tests is "too complicated" or "I have to write so much code". The likelyhood is that the more code you have to write means your code is over complicated or doesn't have ideal design.

So how can we improve this from a test setup perspective?

First off we need to make it easy to swap out the dependencies. To do this I created a simple factory class which inherited from WebApplicationFactory. Once we have that it's simple OOP and we can override the ConfigureWebHost method.

public class CustomWebApplicationFactory<TStartup>
    : WebApplicationFactory<TStartup> where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            // swap out dependencies
        });
    }
}

Now we've got the above we can access the IServiceCollection and swap out the dependencies as before.

To allow for a clean api I added 2 different varients on how to change the service registrations. The two options are either through the constructor or a specific Registration delegate which is run before the host is created.

public class CustomWebApplicationFactory<TStartup>
    : WebApplicationFactory<TStartup> where TStartup : class
{
    public Action<IServiceCollection> Registrations { get; set; }

    public CustomWebApplicationFactory() : this(null)
    {
    }

    public CustomWebApplicationFactory(Action<IServiceCollection> registrations = null)
    {
        Registrations = registrations ?? (collection => { });
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            Registrations?.Invoke(services);
        });
    }
}

How to use the CustomWebApplicationFactory<>

As specified earlier there are two ways of changing the service collection registrations.

You can create the setup as before and then specify an Action delegate to action the swaps before being further processed.

[Fact]
public async Task Test3()
{
    // Arrange
    var factory = new CustomWebApplicationFactory<WebApplication41.Startup>();

    // setup the swaps
    factory.Registrations = services =>
    {
        services.SwapTransient<IMySimpleGithubClient, MockSimpleGithubClient>();
    };

    // Create an HttpClient which is setup for the test host
    var client = factory.CreateClient();

    ** snip for brevity **
}

Or the other way is pass in the Action delegate through the constructor which on initial testing to me seems a cleaner option.

[Fact]
public async Task Test3_1()
{
    // Arrange
    var factory = new CustomWebApplicationFactory<WebApplication41.Startup>(services =>
    {
        // setup the swaps
        services.SwapTransient<IMySimpleGithubClient, MockSimpleGithubClient>();
    });

    // Create an HttpClient which is setup for the test host
    var client = factory.CreateClient();

    ** snip for brevity
}

Conclusion

In this post we have continued looking at integration tests primarily looking at removing the need for a lot of boiler plate code to allow for setup and running of the tests. This have been achieved by using the WebApplicationFactory<> and a derived version for more advanced scenarios.

Any questions/comments then please contact me on Twitter @WestDiscGolf

Part 1 - Integration Testing with ASP.NET Core 3.1

Part 2 - Integration Testing with ASP.NET Core 3.1 - Testing Your Application

Part 3 - Integration Testing with ASP.NET Core 3.1 - Swapping a dependency

Part 4 - This post

Part 5 - Integration Testing with ASP.NET Core 3.1 - Swapping a Dependency with Moq