Skip to content

Recipe Aspnet Integration Test Fixture

Mark Lauter edited this page May 16, 2026 · 1 revision

Recipe: ASP.NET Core integration test fixture

You have an ASP.NET Core service that depends on IAmazonDynamoDB and you want integration tests that exercise the full HTTP stack against a real-shaped DynamoDB — without a container, without AWS credentials, without sharing state across tests. WebApplicationFactory<T> runs the app in-process; replacing the IAmazonDynamoDB registration with one backed by DynamoDbLite gives you a working data layer for the test run.

The interesting wrinkle is the registration order. AddDynamoDbLite calls TryAddSingleton, so it does not overwrite an existing registration. To swap the production client for a test one, the test fixture must RemoveAll<IAmazonDynamoDB>() first, then add the DynamoDbLite registration.

Project setup

NuGet packages are not yet published; the integration test project references DynamoDbLite from source alongside the web project and test packages:

<ItemGroup>
  <ProjectReference Include="..\..\DynamoDbLite\src\DynamoDbLite\DynamoDbLite.csproj" />
  <ProjectReference Include="..\MyApi\MyApi.csproj" />
  <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
  <PackageReference Include="xunit.v3" />
</ItemGroup>

Microsoft.AspNetCore.Mvc.Testing ships WebApplicationFactory<TEntryPoint> and the TestServer it spins up. Once MSL.DynamoDbLite ships, swap the ProjectReference for <PackageReference Include="MSL.DynamoDbLite" />.

The fixture

The fixture extends WebApplicationFactory<Program> (or whatever your entry point class is) and does two things: ConfigureWebHost swaps the IAmazonDynamoDB registration, and IAsyncLifetime.InitializeAsync creates the tables after the host is built.

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using DynamoDbLite;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;

public sealed class MyApiFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly string connectionString =
        $"Data Source=integ_{Guid.NewGuid():N};Mode=Memory;Cache=Shared";

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.RemoveAll<IAmazonDynamoDB>();
            services.AddDynamoDbLite(o => o.WithConnectionString(connectionString));
        });
    }

    public async ValueTask InitializeAsync()
    {
        var client = Services.GetRequiredService<IAmazonDynamoDB>();
        await CreateTables(client);
    }

    public override ValueTask DisposeAsync() => base.DisposeAsync();

    private static async Task CreateTables(IAmazonDynamoDB client)
    {
        await client.CreateTableAsync(new CreateTableRequest
        {
            TableName = "Users",
            KeySchema = [new KeySchemaElement("UserId", KeyType.HASH)],
            AttributeDefinitions = [new AttributeDefinition("UserId", ScalarAttributeType.S)],
            BillingMode = BillingMode.PAY_PER_REQUEST,
        });
    }
}

ConfigureTestServices runs after the production ConfigureServices, so the RemoveAll<IAmazonDynamoDB>() + AddDynamoDbLite(...) pair lands as the final registration of IAmazonDynamoDB before the host is built. InitializeAsync then runs against the already-built host — Services is the live IServiceProvider, not a side provider built from the registration list — so the IAmazonDynamoDB resolved here is the DynamoDbLite-backed one the application will see.

The example fixture declares IAsyncLifetime (xunit.v3) alongside WebApplicationFactory<Program>. WebApplicationFactory<T> lives in Microsoft.AspNetCore.Mvc.Testing and has no xUnit dependency; the IAsyncLifetime interface is what wires the fixture into the xUnit runner. xUnit calls InitializeAsync on the fixture when the IClassFixture<MyApiFactory> is first resolved, and DisposeAsync when the class teardown runs.

The test

public sealed class UsersEndpointTests : IClassFixture<MyApiFactory>
{
    private readonly MyApiFactory factory;

    public UsersEndpointTests(MyApiFactory factory) => this.factory = factory;

    [Fact]
    public async Task Post_User_Then_Get_Returns_Created_User()
    {
        var http = factory.CreateClient();

        var post = await http.PostAsJsonAsync("/users", new { UserId = "u-1", Name = "Alice" });
        post.EnsureSuccessStatusCode();

        var get = await http.GetFromJsonAsync<UserDto>("/users/u-1");

        Assert.NotNull(get);
        Assert.Equal("Alice", get.Name);
    }
}

The fixture is shared across the test class; each class gets its own database via the GUID-suffixed Data Source. For finer-grained isolation, swap IClassFixture for per-test instantiation — the fixture constructor runs again, generating a fresh GUID.

Sharing a database across tests deliberately

When a test sequence needs to build on prior state — a multi-step workflow that posts, then queries, then verifies — keep the IClassFixture pattern and write the tests to flow through the fixture in [Fact] order. xUnit does not guarantee order across facts; for ordered scenarios, use a single [Fact] with multiple steps inside, or a separate test class per scenario.

Cleanup

In-memory databases need no cleanup — disposing WebApplicationFactory (which xUnit does automatically when IClassFixture goes out of scope) disposes the underlying DynamoDbClient, and the GUID-keyed shared cache entry is reclaimed by the GC.

For file-based fixtures (rare in integration tests), apply the cleanup pattern from Recipe: xUnit per-test isolation.

Next steps

Clone this wiki locally