The ASP.NET testing story has gotten easier with each new iteration, emphasizing dependency injection and inversion of control (IoC) containers contributing much of the plumbing for your web applications. Along with more accessible configuration options, we also see more opportunities in ASP.NET Core to access our web application in unit testing scenarios, easing the burden of setting up integration tests. Generally, it has been a positive advancement in the .NET space, which hopefully will lead to better-tested code and fewer bugs in production.

In this post, we’ll go through the steps of setting up an ASP.NET Core web application using minimal APIs/minimal hosting to work with a unit testing library. In this case, we’ll be using XUnit, but this post can be adapted to work with your testing library of choice.

The Web Application Project

You’ll need to start with a straightforward minimal API project. This sample will have a single endpoint with two service dependencies: IMessageService and NameService.

public interface IMessageService
{
    string SayHello();
}

public class MessageService : IMessageService
{
    public string SayHello() => "Hello, World!";
}

public class NameService
{
    public string Name => "Khalid";
}

We’ll be using Minimal Hosting, where our Program class will be generated. It’s important to understand that the Program class will be declared internal. Later, you’ll need to modify the csproj of your web application to allow your test project access to the Program type.

Now, you’ll need to create our application. You can put all of your code in our Program.cs class in this example, along with our services.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMessageService, MessageService>();
builder.Services.AddScoped<NameService>();

var app = builder.Build();

app.MapGet("/", (IMessageService message, NameService names) 
    => $"{names.Name} says \"{message.SayHello()}\"");

app.Run();

public interface IMessageService
{
    string SayHello();
}

public class MessageService : IMessageService
{
    public string SayHello() => "Hello, World!";
}

public class NameService
{
    public string Name => "Khalid";
}

Notice that one of the services, MessageService, implements an interface, while NameService does not. Accordingly, you’ll be replacing the IMessageService dependency in a test while still utilizing the NameService in its current implementation.

Finally, let’s make sure that the Program class is visible to your unit testing project. In your csproj, add the InternalsVisibleTo element, making sure the Include attribute has the assembly name of your test project. Be sure to change the value according to your assembly names.

    <ItemGroup>
        <InternalsVisibleTo Include="TestingWebRequests.Tests" />
    </ItemGroup>

Now we’re ready to write some tests!

The Unit Test Project

You’ll first need to create a unit testing project. As mentioned above, this post uses XUnit, but the steps shown here will work with NUnit and other testing frameworks.

First, you’ll need to add the Microsoft.AspNetCore.Mvc.Testing package. This library holds the WebApplicationFactory<> class, which will allow us to configure our web application under testing conditions.

dotnet add package Microsoft.AspNetCore.Mvc.Testing

Now that you’ve added the package to your unit testing library, you’ll need to create an implementation of WebApplicationFactory<>. In this case, you’ll create a TestApplication class. You’ll also need a new implementation of the IMessageService.

// internal is important as it's the
// same access level as `Program`
internal class TestApplication : WebApplicationFactory<Program>
{
    public string Message { get; set; }
    
    protected override IHost CreateHost(IHostBuilder builder)
    {
        builder.ConfigureServices(s => {
            s.AddScoped<IMessageService, TestMessageService>(
                _ => new TestMessageService {
                Message = Message
            });
        });

        return base.CreateHost(builder);
    }
}

public class TestMessageService : IMessageService
{
    /// <summary>
    /// Allow us to set the message
    /// </summary>
    public string Message { get; set; } = "Hello, World!";

    public string SayHello() => Message;
}

Notice how the base implementation of TestApplication is WebApplicationFactory<Program>. The use of the type here is why you changed the visibility of the Program class. The library uses the marker class to get the assembly reference and read any previous code configuration.

Now, let’s write a test!

public class RootEndpointTests
{
    private readonly ITestOutputHelper output;
    private readonly TestApplication app;

    public RootEndpointTests(ITestOutputHelper output)
    {
        this.output = output;
        app = new TestApplication();
    }
    
    [Fact]
    public async Task Can_get_message()
    {
        app.Message = "test message";
        
        var client = app.CreateDefaultClient();
        var result = await client.GetStringAsync("/");
        
        output.WriteLine(result);
        Assert.Equal($"Khalid says \"{app.Message}\"", result);
    }
}

In the code above, you change the IMessageService implementation to one in which you can modify the message. In the case of NamedService, you want to continue to use the implementation in the original configuration.

Let’s break down the test method into its most essential components:

  1. Creating our TestApplication instance on each test run.
  2. Set our Message as a property of our test application.
  3. Get an in-memory HttpClient to call the root endpoint.
  4. Call the root endpoint and store the result.
  5. Assert the result is correct.

The final code looks like this:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Xunit;
using Xunit.Abstractions;

namespace TestingWebRequests.Tests;

public class RootEndpointTests
{
    private readonly ITestOutputHelper output;
    private readonly TestApplication app;

    public RootEndpointTests(ITestOutputHelper output)
    {
        this.output = output;
        app = new TestApplication();
    }
    
    [Fact]
    public async Task Can_get_message()
    {
        app.Message = "test message";
        
        var client = app.CreateDefaultClient();
        var result = await client.GetStringAsync("/");
        
        output.WriteLine(result);
        Assert.Equal($"Khalid says \"{app.Message}\"", result);
    }
}

internal class TestApplication : WebApplicationFactory<Program>
{
    public string Message { get; set; }
    
    protected override IHost CreateHost(IHostBuilder builder)
    {
        builder.ConfigureServices(s => {
            s.AddScoped<IMessageService, TestMessageService>(
                _ => new TestMessageService {
                Message = Message
            });
        });

        return base.CreateHost(builder);
    }
}

public class TestMessageService : IMessageService
{
    /// <summary>
    /// Allow us to set the message
    /// </summary>
    public string Message { get; set; } = "Hello, World!";

    public string SayHello() => Message;
}

Conclusion

I’m thrilled with the test integration story in ASP.NET Core, and as this post demonstrated, it takes very little code to write and test any existing ASP.NET Core application. Using the example here, you could replace your database, third-party services, or other dependencies with relative ease. I hope you found this post helpful, and please share it with friends and coworkers.