ASP.NET Core Integration Tests using WebApplicationFactory

Integration testing evaluates an application's components at a broader level, ensuring that database connections, routing, middleware, and dependency injection function seamlessly together. In ASP.NET Core, the WebApplicationFactory<TEntryPoint> class from the Microsoft.AspNetCore.Mvc.Testing package enables you to host an in-memory test server and execute HTTP requests against your API without deploying it to an external environment. This guide explains how to set up, configure, and execute integration tests in C# using WebApplicationFactory.

1. Setting Up the Test Project

To get started, create a dedicated test project using your preferred test framework, such as xUnit:

dotnet new xunit -n MyApp.IntegrationTests
cd MyApp.IntegrationTests

Add a project reference to the web application you intend to test:

dotnet add reference ../MyApp/MyApp.csproj

Install the integration testing NuGet package:

dotnet add package Microsoft.AspNetCore.Mvc.Testing

Exposing the Program Class

Modern ASP.NET Core projects use top-level statements where the Program class is generated implicitly as an internal class. To make it accessible to your test project, add the following line to the end of your web application's Program.cs file:

public partial class Program { }

Alternatively, expose internal types to your test project inside the web application's .csproj file:

<ItemGroup>
    <InternalsVisibleTo Include="MyApp.IntegrationTests" />
</ItemGroup>

2. Writing a Basic Integration Test

Using WebApplicationFactory<Program>, you can spin up the application in-memory and obtain an HttpClient instance configured to communicate with the test server.

using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;

namespace MyApp.IntegrationTests;

public class BasicEndpointTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicEndpointTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task Get_HealthCheck_ReturnsSuccessAndCorrectContentType()
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync("/health");

        // Assert
        response.EnsureSuccessStatusCode();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType?.ToString());
    }
}

3. Customizing the WebApplicationFactory

Most real-world integration tests require overriding specific dependencies, such as using an in-memory or test database, or replacing third-party API clients with mock implementations. You can achieve this by subclassing WebApplicationFactory<Program>.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MyApp.Data;

namespace MyApp.IntegrationTests;

public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove the application's default DbContext registration
            var descriptor = services.SingleOrDefault(d => 
                d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));

            if (descriptor != null)
            {
                services.Remove(descriptor);
            }

            // Register an in-memory database for testing
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseInMemoryDatabase("InMemoryDbForTesting");
            });

            // Seed test data if required
            var sp = services.BuildServiceProvider();
            using var scope = sp.CreateScope();
            var scopedServices = scope.ServiceProvider;
            var db = scopedServices.GetRequiredService<ApplicationDbContext>();
            db.Database.EnsureCreated();
            
            SeedTestData(db);
        });
    }

    private static void SeedTestData(ApplicationDbContext db)
    {
        if (!db.Users.Any())
        {
            db.Users.Add(new User { Id = 1, Name = "Test User", Email = "test@example.com" });
            db.SaveChanges();
        }
    }
}

4. Writing Tests with the Custom Factory

Once the custom factory is defined, implement tests that interact with endpoints, verify payload responses, and assert against database states:

using System.Net;
using System.Net.Http.Json;
using MyApp.Models;
using Xunit;

namespace MyApp.IntegrationTests;

public class UserEndpointTests : IClassFixture<CustomWebApplicationFactory>
{
    private readonly HttpClient _client;

    public UserEndpointTests(CustomWebApplicationFactory factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetUserById_ReturnsUser_WhenUserExists()
    {
        // Act
        var response = await _client.GetAsync("/api/users/1");

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        
        var user = await response.Content.ReadFromJsonAsync<UserDto>();
        Assert.NotNull(user);
        Assert.Equal("Test User", user.Name);
    }

    [Fact]
    public async Task CreateUser_ReturnsCreated_WhenPayloadIsValid()
    {
        // Arrange
        var newUser = new CreateUserRequest
        {
            Name = "Jane Doe",
            Email = "jane@example.com"
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/users", newUser);

        // Assert
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);

        var createdUser = await response.Content.ReadFromJsonAsync<UserDto>();
        Assert.NotNull(createdUser);
        Assert.Equal("Jane Doe", createdUser.Name);
        Assert.NotNull(response.Headers.Location);
    }
}

5. Overriding Environment and Configuration

You can also override appsettings values or specify an environment (such as Testing) directly within your factory:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    builder.UseEnvironment("Testing");

    builder.ConfigureAppConfiguration((context, config) =>
    {
        config.AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["FeatureFlags:EnableBetaFeature"] = "true",
            ["ExternalService:ApiKey"] = "test-api-key"
        });
    });
}

This structure ensures that tests execute in an isolated environment that closely mirrors production without relying on external infrastructure.