Introduction to WebApplicationFactory

This article will show you what exactly a WebApplicationFactory is and why it is so "mighty" when it comes down to testing. I will highlight some of the properties which you can leverage to write powerful tests.

Motivation

Before I go into detail how that WebApplicationFactory works, I want to first tell you why I use it and also why you should consider using it. We all know unit test, integration tests or even end to end tests. Now what they particual mean, is always subjective. Ask 10 developers, and you will get a lot of different answers. But still, I hope we have a common understanding or direction when we talk about those words.

Anyway, your application consists of smaller chunks of information, which compose the bigger picture. Now it doesn't matter if you follow onion architecture, clean architecture, screaming architecture or something completely else, but surely we can agree that we need to test those things. Those architectures just give us guidelines to scale better and write more maintainable code. More maintainable code often translates to easier testable code. When we test our code, there are different levels as stated above. Those tests (unit, integration) are also an abstraction to the detail you work on.

At the end of the day your user doesn't care about that stuff. Your user cares if sends a request to your endpoint that there is something useful coming back. So it does make sense, if we write tests, to have the same position as the user. So our test-client is "another" real client next to your user. That has some advantages like:

  • If your contract changes (like in a WebAPI) the test fails as well.
  • You will test the whole chain and you can write tests from an user point of view (without relying on any technical details)

Also see it like that, if you write tests at that high level (from a user journey point of view) you have a very good overview of your features. So you build up a living documentation of your code. That is awesome. Side note: I am a big fan of Feature folders.

This is where the WebApplicationFactory comes into play:

WebApplicationFactory

Introduced with .net core 2.1 WebApplicationFactory is oversimplified an in-memory server which starts your ASP.NET Core WebAPI. This will call your regular Startup class as well as your DI and will go against the real database (if not configured differently). It also parses your application.json configuration and passes it to your application. So it is a real server running and hosting your code.

So I will show you some code examples which are working on a regular ASP.NET Core WebAPI, which then gets tested by the WebApplicationFactory. As always you will find the source to that example at the end of the blog post.

So here is the super small MinimalAPI we want to test:

using WebAppFactory;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/", (HelloWorldRequest request) => $"Hello {request.Name}!");

app.Run();

With the following easy HelloWorldRequest:

public class HelloWorldRequest
{
    public string Name { get; set; }
}

So let's set up a regular xUnit test (or nUnit, MSTest, ... whatever you prefer). We are done. Well almost, we need the nuget package to make it work: <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.9" />. Another thing we have to consider is the reference to our "productive" project. With that done, we can write our "setup" code. That is a bit different than a regular unit test. In xUnit you utilize the constructor, but take whatever pleases you. We just need a point before the first test where we can initialize stuff:

// The IClassFixture<WebApplicationFactory<Program>> annotation is to tell xUnit
// to provide a WebApplicationFactory<Program> to the constructor
public class FeatureOneTests : IClassFixture<WebApplicationFactory<Program>>
{
    public void GreetingUserWithHisName(WebApplicationFactory<Program> factory)
    {
    }
}

Two things:

  • You see I named my class (and following also my methods) like the feature I want to test. That is important if you want to have a living documentation inside your code.
  • That code would not compile. The reason is that Program is internal. You see, if you create the Minimal API template then you don't have an obvious Main method anymore so the compiler creates one from you. Unfortunately this one is internal. So one possible fix would be adding the InternalsVisibleTo but that brings a lot of boiler plate code with it. The reason is that still your test has to be internal, but internal stuff can't be executed by the test runner. So we can make a simple fix: We force the compiler via partial class to make Program public. That is really the simplest thing, even though not perfect
using WebAppFactory;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/", (HelloWorldRequest request) => $"Hello {request.Name}!");

app.Run();

// This will force the compiler to generate a public class instead of an internal one
public partial class Program
{
}

Okay with the compiler error gone, let's finish what we started:

// The IClassFixture<WebApplicationFactory<Program>> annotation is to tell xUnit
// to provide a WebApplicationFactory<Program> to the constructor
public class FeatureOneTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient client;

    public FeatureOneTests(WebApplicationFactory<Program> factory)
    {
        client = factory.CreateClient();
    }

Now there is a bit more what you can do, but for the easy scenarios you can now use client to do all of your stuff. We are good to go. So let's write our first test (I will comment the code to give you a good understanding, it isn't that complicate, I promise):

[Fact]
public async Task PassingNameShouldReturnNiceWelcomeMessage()
{
    var response = await client.PostAsJsonAsync("/", new
    {
        Name = "Steven"
    });

    Assert.True(response.IsSuccessStatusCode);
    var content = await response.Content.ReadAsStringAsync();
    Assert.Equal("Hello Steven!", content);
}

Now you might ask yourself why did I pass in an anonymous object, where my domain object is publicly available? Multiple reasons:

  • Yes it is publicly available, but that is not the point of using it. Remember, we are a user of the API. Your user does not know the internal representation of your domain objects.
  • Our test should not automatically get refactored when we change our domain model. We want to see "breaking changes" in some level. An anonymous object does that for use perfectly. Imagine a property name changes from Name to Fullname and you use refactoring tools like Resharper. All your code will work fine afterwards, because your tests get refactored as well. But your customers API call maybe does not. So we should behave like the worst user in that case.
  • We test serialization and deserialization. This is especially important if you have custom logic around that topic or you want to migrate from one serializer to another (for example: Newtonsoft.Json to System.Text.Json)

If you run that thing (without running your main application logic) this test will turn green!

Custom configuration/environment

When you create the WebApplicationFactory, there are many options to the builder, which you can change. Let's have a look at the most important ones:

public WebAPITests(WebApplicationFactory<Program> factory)
{
    this.factory = factory.WithWebHostBuilder(builder =>
    {
        // Change a setting from your application.json
        builder.UseSetting("ConnectionString", "file=:memory:");
        // You can also go into nested objects with the : notation
        // If you have a appsettings.json, which looks like this:
        // MyObject {
        //   MyProp: 123
        // }
        // You can change it like that:
        builder.UseSetting("MyObject:MyProp", 234);

        // You can also change the environment to load for example a appsettings.tests.json
        // Or if you have switches in your code where you rely on that
        builder.UseEnvironment("tests");

        // And you can reconfigure services if you want.
        // So still you have the ability to pass in Fakes
        builder.ConfigureServices();
    });
}

Closing

This article gives a nice and short introduction to the WebApplicationFactory which can help you out to get a better understanding of your code.

Resources

  • Source code for that article can be found here
  • The majority of source code in my articles can be found here
6
An error has occurred. This application may no longer respond until reloaded. Reload x