In modern software development, ensuring your application works as a whole is just as critical as testing its individual parts. While unit tests verify the logic of individual components in isolation, integration tests confirm that these components work together correctly. For ASP.NET Core developers, there’s a first-class tool designed specifically for this: WebApplicationFactory.

If you’ve struggled with setting up complex test environments, managing test databases, or mocking services for your API tests, WebApplicationFactory solves these problems. This guide covers everything you need to write clean, fast, and reliable integration tests for your ASP.NET Core APIs.

What is Integration Testing?

In an ASP.NET Core API context, an integration test sends an HTTP request to an endpoint and asserts that the response is correct. This exercises the entire request pipeline: routing, model binding, business logic, database access, and serialization, all without deploying to a real web server.

This approach gives you high confidence that your application layers work together correctly.

The Old Way vs. The WebApplicationFactory Way

Traditional integration testing was painful:

  • Spinning up a real web server like Kestrel
  • Manually configuring connection strings for test databases
  • Writing complex setup and teardown scripts
  • Slow execution times and brittle tests that failed due to environmental issues

WebApplicationFactory changes this completely. Part of the Microsoft.AspNetCore.Mvc.Testing NuGet package, it bootstraps your application in-memory. It creates a TestServer that hosts your app and provides an HttpClient to make requests against it.

Benefits:

  • No real network port is used. Tests are faster and more reliable.
  • Complete control over the dependency injection container. You can replace real services like databases or external API clients with test-specific versions.
  • Simplified configuration managed entirely within your test project.

From Experience: I’ve seen teams cut integration test execution time by 70% after switching from real server deployments to WebApplicationFactory. The in-memory approach also eliminated flaky tests caused by port conflicts and network timeouts.

Setting Up the Projects

Start with a standard setup: your ASP.NET Core API project and a separate test project using xUnit, NUnit, or MSTest.

API Project (MyApi.csproj)

This is your standard ASP.NET Core Web API project.

Test Project (MyApi.Tests.csproj)

Add references to your API project and the necessary testing packages:

<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
  <PackageReference Include="xunit" Version="2.5.3" />
  <PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
  <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
  <PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>

<ItemGroup>
    <ProjectReference Include="..\MyApi\MyApi.csproj" />
</ItemGroup>

To allow WebApplicationFactory to discover and build your API project, make the Program class accessible to the test project. Add this to your API project’s .csproj file:

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

Writing a Basic Integration Test

Assume your API has this simple endpoint:

// GET /api/weather
app.MapGet("/api/weather", () =>
{
    var forecast = new { Temperature = 32, Summary = "Sunny" };
    return Results.Ok(forecast);
});

Here’s how to test it with xUnit:

using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using FluentAssertions;

namespace MyApi.Tests;

public class WeatherApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public WeatherApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

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

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        response.Content.Headers.ContentType?.MediaType.Should().Be("application/json");
    }

    [Fact]
    public async Task GetWeather_ReturnsExpectedForecast()
    {
        // Act
        var forecast = await _client.GetFromJsonAsync<WeatherForecast>("/api/weather");

        // Assert
        forecast.Should().NotBeNull();
        forecast.Temperature.Should().Be(32);
        forecast.Summary.Should().Be("Sunny");
    }
}

public record WeatherForecast(int Temperature, string Summary);

Key points:

  1. IClassFixture<WebApplicationFactory<Program>> injects a factory instance shared across all tests in this class
  2. factory.CreateClient() provides an HttpClient pre-configured to send requests to the in-memory test server
  3. Use this client like any other HttpClient to make requests and assert responses

Customizing the Factory for Dependencies

Real applications have databases, third-party services, and external resources. You don’t want integration tests to rely on these. Customizing WebApplicationFactory solves this problem.

Assume your API has a POST endpoint that saves a product to a SQL Server database using EF Core. Replace the real database with an in-memory database for tests.

Create a Custom Factory

Create a custom factory class that inherits from WebApplicationFactory<Program>:

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

namespace MyApi.Tests;

public class CustomApiFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove the real DbContext registration
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));

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

            // Add in-memory database
            services.AddDbContext<AppDbContext>(options =>
            {
                options.UseInMemoryDatabase("InMemoryDbForTesting");
            });

            // Seed the database
            var sp = services.BuildServiceProvider();
            using var scope = sp.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            db.Database.EnsureCreated();
        });
    }
}

Use the Custom Factory in Tests

Update your test class to use CustomApiFactory:

public class ProductsApiTests : IClassFixture<CustomApiFactory>
{
    private readonly HttpClient _client;
    private readonly CustomApiFactory _factory;

    public ProductsApiTests(CustomApiFactory factory)
    {
        _factory = factory;
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreateProduct_WhenModelIsValid_ReturnsCreated()
    {
        // Arrange
        var newProduct = new { Name = "Test Gadget", Price = 99.99 };

        // Act
        var response = await _client.PostAsJsonAsync("/api/products", newProduct);
        
        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Created);
        
        var createdProduct = await response.Content.ReadFromJsonAsync<Product>();
        createdProduct.Should().NotBeNull();
        createdProduct.Name.Should().Be("Test Gadget");

        // Verify it was saved to the in-memory database
        using var scope = _factory.Services.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var productInDb = await context.Products.FindAsync(createdProduct.Id);

        productInDb.Should().NotBeNull();
        productInDb.Name.Should().Be("Test Gadget");
    }
}

By overriding ConfigureWebHost, you intercept the application’s startup process and swap the real database service for a test-friendly in-memory version. Use this same pattern to:

  • Mock an IEmailSender service to prevent sending real emails
  • Replace an IHttpClientFactory client that calls a third-party API with a mock handler
  • Modify IConfiguration values for testing purposes

Pro Tip: In production systems, I always create a base test factory class that handles common service replacements (email, external APIs, file storage). Individual test classes inherit from it and add their specific overrides. This keeps test code DRY and maintainable.

Handling Authentication

Testing secure endpoints is common. Create a test authentication handler that bypasses the real authentication scheme and provides a pre-defined user for tests.

Here’s the approach within your custom factory:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    builder.ConfigureServices(services =>
    {
        services.AddAuthentication("Test")
            .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", options => { });
    });
}

Create a TestAuthHandler that returns a successful authentication result with a ClaimsPrincipal containing the claims your test needs. This lets you test authorization logic without setting up real JWT tokens or cookies.

Performance Considerations

Integration tests with WebApplicationFactory are significantly faster than tests using real servers, but they’re still slower than unit tests. Here are some benchmarks from a real project:

Test TypeAverage Execution TimeTests per Minute
Unit Test5ms12,000
Integration Test (WebApplicationFactory)150ms400
Integration Test (Real Server)800ms75

To keep your test suite fast:

  • Use test collections to parallelize tests where possible
  • Share factory instances across test classes with IClassFixture
  • Keep database seeding minimal, only add data needed for specific tests
  • Consider using Respawn or similar tools to reset database state between tests instead of recreating the context

Common Pitfalls to Avoid

Database context lifetime issues: If you’re getting disposed context errors, make sure you’re creating a new scope for each database operation in your tests.

Port conflicts in CI/CD: Even though WebApplicationFactory doesn’t use real ports, some CI environments have restrictions. Use WithWebHostBuilder to explicitly disable server features if needed.

Test isolation problems: Each test should be independent. Avoid sharing state through static fields or singleton services that maintain state.

Conclusion

WebApplicationFactory is an indispensable tool in the ASP.NET Core ecosystem. It enables fast, robust, and maintainable integration tests by providing a fully-featured, in-memory representation of your application. By mastering the ability to customize its service container, you can confidently test complex application workflows and ensure all the pieces of your API fit together perfectly.

After using this approach across multiple production systems, I can say it’s transformed how teams approach API testing. Tests run faster, fail less frequently, and catch real integration issues before they reach production.

References

Related Posts