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

Looking at how you can write integration tests for your application but swapping out a 3rd party dependency

Published on 11 December 2019

Now we have an integration test which starts up a test server and bootstraps our application we're on a roll. However we maybe dependent on a database connection. This can be achieved by creating an in memory database and seeding some data into it. I'm not going to cover this today. I am however going to cover the scenario where you are dependent on an external resource.

Picture the scene, you've got a system which makes requests out to an external 3rd party. You have no control if/when their test system stays up but you want to test different scenarios from the call to them. How do we test this?

Let's find out!

Simple Scenario

We've got a simple Github strongly typed HttpClient based service which we are able to inject into our services. This is defined by a simple interface, has a basic implementation and returns a simple strongly typed response.

public interface IMySimpleGithubClient
{
    Task<GithubUser> GetUserAsync(string name);
}

public class MySimpleGithubClient : IMySimpleGithubClient
{
    private readonly HttpClient _httpClient;

    public MySimpleGithubClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<GithubUser> GetUserAsync(string name)
    {
        var response = await _httpClient.GetAsync($"/users/{name}");
        var responseAsString = await response.Content.ReadAsStringAsync();
        return System.Text.Json.JsonSerializer.Deserialize<GithubUser>(responseAsString);
    }
}

public class GithubUser
{
    [JsonPropertyName("name")]
    public string Name { get; set; }
}

We've registered this in the Startup class using the AddHttpClient extension method and it is injected into the Controller action through the [FromServices] attribute to make the example clearer. This could also be done through constructor injection which is my usual preferred method, especially for none controller service dependencies.

public async Task<IActionResult> Index([FromServices] IMySimpleGithubClient client)
{
    return Ok(await client.GetUserAsync("WestDiscGolf"));
}

Now we have the client setup if we run the web application we can receive the Name property back from my Github profile.

Happy days!

How to test without Github connectivity?

The normal scenario for this type of setup is you have made a request many layers down, with authentication header middleware etc. and then the returned data from the 3rd party bubbles back up into the service and your code works with it in a certain way. But you don't want your application integration tests being reliant on an external system, so what can we do?

What we need to do is stub out the client. In this case the we need to create a mock of IMySimpleGithubClient which can be used intead. This can be setup to return specific results to mimic the 3rd party api.

public class MockSimpleGithubClient : IMySimpleGithubClient
{
    public Task<GithubUser> GetUserAsync(string name)
    {
        return Task.FromResult(new GithubUser { Name = "My Test User" });
    }
}

Now we have a mocked Github client how do we add it into the Ioc container? We do this in the host builder setup.

Note: If you are doing more complex processing in your strongly typed client, the mocked abstraction can go down another layer into the Http pipeline. To do this you have to mock out the delegating handler which the underlying SendAsync method calls.

Swapping the application dependency

During the host builder setup we have access to the ConfigureTestServices method. This allows for items to be registered into the webhost. If you call it after the UseStartup method you can access the IServiceCollection which has been generated before it us used to created a service provider.

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

    });

As you can see from the above I have swapped out the production concrete IMySimpleGithubClient implementation with the mocked version. This is an extension method which I have written to allow to do this.

Swap Extension Method

To swap items in/out of the IServiceCollection you need to know a few things. One is the service type, also known as TService, and the ServiceLifetime. Knowing both of these bits of information you can do a check and remove the items.

/// <summary>
/// Removes all registered <see cref="ServiceLifetime.Transient"/> registrations of <see cref="TService"/> and adds in <see cref="TImplementation"/>.
/// </summary>
/// <typeparam name="TService">The type of service interface which needs to be placed.</typeparam>
/// <typeparam name="TImplementation">The test or mock implementation of <see cref="TService"/> to add into <see cref="services"/>.</typeparam>
/// <param name="services"></param>
public static void SwapTransient<TService, TImplementation>(this IServiceCollection services) 
    where TImplementation : class, TService
{
    if (services.Any(x => x.ServiceType == typeof(TService) && x.Lifetime == ServiceLifetime.Transient))
    {
        var serviceDescriptors = services.Where(x => x.ServiceType == typeof(TService) && x.Lifetime == ServiceLifetime.Transient).ToList();
        foreach (var serviceDescriptor in serviceDescriptors)
        {
            services.Remove(serviceDescriptor);
        }
    }

    services.AddTransient(typeof(TService), typeof(TImplementation));
}

This method will only work when there is 1 type of registration for the specific service type. If you are trying to replace items which have multiple implementations of the same interface then you will need to do some more advance juggling!

The rest of the test is similar to before.

The Integration Test in Full

Let's take a look at the full integration test.

[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 before I still run with the AAA (Arrange, Act, Assert) testing style.

How is this different to a unit test?

You maybe looking at the test above and thinking "But can't I just test this by calling the action method directly on the controller?". The difference is is that would be a unit test on the controller action. You could mock/stub out the dependency it is calling and check the different return values. However as it was calling the method directly it would not be actioning any of the surrounding infrastructure. It would not be testing the controller being instantiated through the DI. It would not be not be testing the routing. It would not be testing the middleware process, etc.

Having said that this should not be used instead of a unit test for controller actions. These tests work together so ideally you should look to do both!.

Conclusion

In this post we have advanced the integration test to exercise all of the application code for the specific flow except for the dependency which calls out to an external 3rd party api. This allows for integration testing without being dependent on an external source, but also allow for mimicing different responses from the 3rd party.

As you can see this is starting to have a lot of boilerplate code so in future posts I will look to see if this can be addressed (spolier, it can!).

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 - This post

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

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