Setting up integration tests using the ASP.NET Core WebHostBuilder

Reason

It’s a real pleasure to write tests for ASP.NET Core, be it unit or integration.

When it comes to writing integration tests, the two main concerns are wiring up an application which behaves as close to the real deal as possible, and replacing any required or desired parts with mocks.

In the following I’ve summarized how we simplify writing integration tests for our service and data layers by using the standard WebHostBuilder to do the grunt work of wiring up the dependencies for our SUT.

Code

Our HttpContext-agnostic integration tests are all set up following the same pattern:

  1. Wire up a WebHostBuilder which in turn builds a IWebHost.
    1.1 Default services are automatically added using the Configure method of the application’s startup class.
    1.2 Mocks/stubs/spies are injected as needed.
  2. Use the IServiceProvider exposed via the IWebHost.Services property to get the SUT.
  3. Run tests against the SUT.

The following code uses NUnit for assertions and NSubstitute for mocking.

WebHostBuilderFactory

The WebHostBuilderFactory facilitates wiring up a WebHostBuilder with any modifications required by our tests, e.g. replacing dependencies with mocks, stubs or spies:

using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

internal static class WebHostBuilderFactory
{
    public static IWebHostBuilder Create<TStartup>(
        params Action<IServiceCollection>[] testSpecificServiceConfigurations) 
        where TStartup : class
    {
        var contentRootPath = Path.Combine(Directory.GetCurrentDirectory(), @"..\..\src\App");
        return new WebHostBuilder().UseContentRoot(contentRootPath)
            .UseEnvironment(EnvironmentName.Development)
            .UseStartup<TStartup>()
            .ConfigureServices(services =>
            {
                // Modify services as required.
                foreach (var serviceConfiguration in testSpecificServiceConfigurations)
                {
                    serviceConfiguration(services);
                }
            });
    }
}

ServiceCollectionExtensions

The ServiceCollectionExtensions facilitate mock injection. Having a toolbox of extension methods to replace often used services helps keep the test suite readable and to the point:

using System;
using Microsoft.AspNet.Authentication;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;

internal static class ServiceCollectionExtensions
{
    public static void Replace<TRegisteredType>(this IServiceCollection services, TRegisteredType replacement)
    {
        for (var i = 0; i < services.Count; i++)
        {
            if (services[i].ServiceType == typeof (TRegisteredType))
            {
                services[i] = new ServiceDescriptor(typeof (TRegisteredType), replacement);
            }
        }
    }

    public static void SetSystemClock(this IServiceCollection services, DateTime date)
    {
        var clock = Substitute.For<ISystemClock>();
        clock.UtcNow.Returns(new DateTimeOffset(date, TimeZoneInfo.Local.GetUtcOffset(date)));
        services.Replace(clock);
    }
}

Example

The following is a simple test which uses the code shown above to create a ServiceProvider wired up with all the default services of the application and mocked versions of IMyDependency and ISystemClock:

[Test]
public void MyServiceDoesAwesomeStuffTwoYearsAgo()
{
    // Arrange
    var myDependency = Substitute.For<IMyDependency>();
    var webHostBuilder = WebHostBuilderFactory.Create<MyStartup>(
             services => services.Replace<IMyDependency>(myDependency), 
             services => services.SetSystemClock(DateTime.Now.AddYears(-2)));
    var serviceProvider = webHostBuilder.Build().Services;
    var myService = serviceProvider.GetRequiredService<IMyService>();
    var expected = "awesome stuff";

    // Act
    var actual = myService.DoStuff();

    // Assert
    Assert.That(actual, Is.EqualTo(expected));
}