Vaughan

ASP.NET Core integration tests with WebApplicationFactory

.NET Core, Testing

We all agree that we want to have tests that cover as much of our application as possible. Right? The general approach with ASP.NET applications has been to extract meaningful business logic out of your controllers. Pretty much everything that needs to be tested would be injected with an interface so that its easy to mock and test your business logic.

There have always been a few open questions when only taking this approach.

  1. The application depends on certain interfaces being registered in startup. How can I test that in a meaningful and non brittle way? If you ignore that, its definitely something that will crash you application as it starts up in DEV (Not production right??).
  2. The HTTP endpoints is the contract with your user. If you like your users then surely you should have tests that cover the expected return types that a user will get?
  3. What if an endpoint is meant to be only accessed by super users. How would you create a test that ensures that normal users cannot access the link?
  4. ASP.NET can also do quite a lot with model validation and action filters. Surely that is logic that needs to be tested?

The answer to this is of course to write integration tests. ASP.NET Core has a great class that helps with this, its called WebApplicationFactory. It creates a in memory HTTP server that you can use with your actual startup class with. You are also able to override service registrations if you want to say mock out your database connection to use an in memory implementation.

To use a simple example, I created a simple action that validates an age. Admittedly this isn't a great example of best practices but it helps show the point.


[Route("api/[controller]")]
[ApiController]
public class PersonController : ControllerBase
{
    [Route("ValidateAge")]
    public async Task<IActionResult> ValidateAge(int age)
    {
        if (age < 0)
        {
            return BadRequest("age cannot be negative");
        }
        return Ok(true);
    }
}

To make sure this is working correctly I created a NUnit test fixture that hits the endpoint with a valid and invalid call. You can see that its really not a lot of code to confirm that you return a BadRequest response for negative numbers.


public class ValidationTests
{
    private WebApplicationFactory<Startup> WebAppFactoryObj;

    [SetUp]
    public void SetupFixture()
    {
        WebAppFactoryObj = new WebApplicationFactory<Startup>()
            .WithWebHostBuilder(
                builder =>
                {
                    builder.ConfigureTestServices(services => {});
                });
    }

    [Test]
    public async Task Returns_bad_request_if_negative()
    {
        using (var httpClient = WebAppFactoryObj.CreateClient())
        {
            var response = await httpClient.GetAsync("api/Person/ValidateAge?age=-10");
            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
        }
    }

    [Test]
    public async Task Returns_ok_if_positive()
    {
        using (var httpClient = WebAppFactoryObj.CreateClient())
        {
            var response = await httpClient.GetAsync("api/Person/ValidateAge?age=100");
            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
        }
    }
}

One common issue people often get stuck with is when you have some sort of external authentication. How do you test something that needs an authenticated user? In my ConfigureServices and Configure methods I added Identity server authentication and enabled the Authentication and Authorization middleware.

public void ConfigureServices(IServiceCollection services)
{
    service.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
        .AddIdentityServerAuthentication(options =>
        {
            options.SupportedTokens = SupportedTokens.Reference;
            options.TokenRetriever = request => TokenRetrieval.FromAuthorizationHeader()(request);
            options.Authority = "https://identitytokenserver"; //use your real server

            // Reference Tokens - will contact the introspection endpoint found in the discovery document to validate the token
            options.ApiName = "myapi"; //use real values
            options.ApiSecret = "myapisecret"; //use real values
        });

    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Then I added an Action to the PersonController to create a new person for authenticated users only.


[Route("api/[controller]")]
[ApiController]
public class PersonController : ControllerBase
{
    [Route("ValidateAge")]
    public async Task<IActionResult> ValidateAge(int age)
    {
        if (age < 0)
        {
            return BadRequest("age cannot be negative");
        }
        return Ok(true);
    }

    [Route("new")]
    [Authorize]
    public async Task<IActionResult> NewPerson()
    {
        return Ok(new Person());
    }
}

public class Person
{

}

The integration test will now give a 403 - Forbidden response when calling the endpoint normally and so you need to add a new mock authentication option to the mock services registration that only the tests can use. I created an extension method that added it to the service collection.


public static class MockAuthentication
{
    public static void Enable(IServiceCollection services)
    {
        services.AddAuthentication("Test")
            .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                "Test", options => {});
    }
}

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
    ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
    : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");
        var result = AuthenticateResult.Success(ticket);
        return Task.FromResult(result);
    }
}

To be clear, this is only used in your unit tests. Do not put this in your main application! I then use it in a new test fixture to confirm that it works.


public class AuthenticationTests
{
    private WebApplicationFactory<Startup> WebAppFactoryObj;

    [SetUp]
    public void SetupFixture()
    {
        WebAppFactoryObj = new WebApplicationFactory<Startup>()
            .WithWebHostBuilder(
                builder =>
                {
                    builder.ConfigureTestServices(services =>
                    {
                        MockAuthentication.Enable(services);
                    });
                });
    }

    [Test]
    public async Task Returns_person_when_authorized()
    {
        using (var httpClient = WebAppFactoryObj.CreateClient())
        {
            var response = await httpClient.GetAsync("api/Person/new");
            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
        }
    }
}

Related Posts

BMC logoBuy me a coffee