๐Ÿ“š Example

April 6, 2026 ยท View on GitHub

This example walks through a complete, real-world integration test setup: a web API that creates users in a SQL Server database.

The full working code is available in the Samples/NotoriousTest.Sample.XUnit folder.


The Application Under Test

The application exposes a single endpoint that inserts a user into a SQL Server database, reading the connection string from IConfiguration:

// Controllers/UserController.cs
[ApiController]
[Route("users")]
public class UserController : ControllerBase
{
    private readonly IConfiguration _configuration;

    public UserController(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    [HttpPost]
    public void CreateUser()
    {
        using var connection = new SqlConnection(_configuration.GetConnectionString("SqlServer"));
        connection.Open();

        using var command = connection.CreateCommand();
        command.Parameters.AddWithValue("@username", $"user_{Random.Shared.Next()}");
        command.Parameters.AddWithValue("@email", $"user_{Random.Shared.Next()}@example.com");
        command.Parameters.AddWithValue("@password_hash", "hash");
        command.Parameters.AddWithValue("@created_at", DateTime.UtcNow);
        command.CommandText = @"INSERT INTO Users (username, email, password_hash, created_at)
                                VALUES (@username, @email, @password_hash, @created_at)";
        command.ExecuteNonQuery();
    }
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();

public partial class Program { }

Setup

Create an xUnit test project and install the required packages:

dotnet add package NotoriousTest.XUnit
dotnet add package NotoriousTest.SqlServer
dotnet add package NotoriousTest.Web

Step 1 โ€” Define the Infrastructure

Inherit from SqlServerContainerInfrastructure. Override Initialize() to create the schema after the database is ready.

public class SqlServerInfrastructure : SqlServerContainerInfrastructure
{
    public SqlServerInfrastructure(EnvironmentId contextId, ITestLogger logger, IRegistry registry)
        : base(contextId, logger, registry) { }

    // Key used to expose the connection string to the web application
    protected override string ConnectionStringKey => "ConnectionStrings:SqlServer";

    public override async Task Initialize()
    {
        await base.Initialize(); // starts container, creates database, outputs connection string

        using var connection = GetDatabaseConnection();
        await connection.OpenAsync();

        using var command = connection.CreateCommand();
        command.CommandText = @"
            CREATE TABLE Users (
                user_id       INT IDENTITY(1,1) PRIMARY KEY,
                username      NVARCHAR(50)  NOT NULL UNIQUE,
                email         NVARCHAR(100) NOT NULL UNIQUE,
                password_hash NVARCHAR(255) NOT NULL,
                created_at    DATETIME DEFAULT GETDATE()
            )";
        await command.ExecuteNonQueryAsync();
    }
}

What base.Initialize() does automatically:

  • Starts a SQL Server Docker container via Testcontainers.
  • Creates a unique database named NotoriousDb_{EnvironmentId}.
  • Registers the database in DoggyDog for crash recovery.
  • Publishes the connection string as a ConfigurationEntry under ConnectionStrings:SqlServer.

What happens on Reset() (before each test):

  • Respawn empties all tables, leaving the schema intact.

What happens on Destroy() (end of session):

  • The Docker container is stopped and removed.

Step 2 โ€” Define the Web Application

public class TestWebApplication : WebApplication<Program>
{
    // Override WebApplicationFactory methods here if needed
}

WebApplication<Program> automatically receives all ConfigurationEntry objects produced by other infrastructures and injects them into the app's IConfiguration. The ConnectionStrings:SqlServer entry published by SqlServerInfrastructure will be available to the controller without any extra wiring.


Step 3 โ€” Create the Environment

public class TestEnvironment : NotoriousTest.XUnit.Environment
{
    public TestEnvironment(IMessageSink sink) : base(sink) { }

    public override Assembly CurrentAssembly => Assembly.GetExecutingAssembly();

    public override async Task ConfigureEnvironment()
    {
        AddInfrastructure<SqlServerInfrastructure>();
        this.AddWebApplication<TestWebApplication>();
    }
}

The environment:

  1. Resolves SqlServerInfrastructure via the internal DI container (all constructor parameters injected automatically).
  2. Initializes infrastructures in order โ€” SqlServerInfrastructure first (produces config), WebApplicationInfrastructure last (consumes it).
  3. Launches DoggyDog to watch the process and clean up containers on crash.

Step 4 โ€” Write the Tests

public class UserTests : NotoriousTest.XUnit.IntegrationTest<TestEnvironment>
{
    public UserTests(TestEnvironment environment) : base(environment) { }

    [Fact]
    public async Task CreateUser_ShouldInsertOneRow()
    {
        // Act โ€” call the API
        HttpClient client = CurrentEnvironment.GetWebApplication().HttpClient;
        HttpResponseMessage response = await client.PostAsync("users", null);

        // Assert โ€” HTTP response
        Assert.True(response.IsSuccessStatusCode);

        // Assert โ€” database state
        SqlServerInfrastructure db = CurrentEnvironment.GetInfrastructure<SqlServerInfrastructure>();
        await using var connection = db.GetDatabaseConnection();
        await connection.OpenAsync(TestContext.Current.CancellationToken);

        using var command = connection.CreateCommand();
        command.CommandText = "SELECT COUNT(*) FROM Users";
        int count = (int)await command.ExecuteScalarAsync(TestContext.Current.CancellationToken);

        Assert.Equal(1, count);
    }

    [Fact]
    public async Task CreateUser_CalledTwice_ShouldStillHaveOneRow()
    {
        // Each test starts with a clean database โ€” Respawn ran before this test
        HttpClient client = CurrentEnvironment.GetWebApplication().HttpClient;
        await client.PostAsync("users", null);

        SqlServerInfrastructure db = CurrentEnvironment.GetInfrastructure<SqlServerInfrastructure>();
        await using var connection = db.GetDatabaseConnection();
        await connection.OpenAsync(TestContext.Current.CancellationToken);

        using var command = connection.CreateCommand();
        command.CommandText = "SELECT COUNT(*) FROM Users";
        int count = (int)await command.ExecuteScalarAsync(TestContext.Current.CancellationToken);

        Assert.Equal(1, count);
    }
}

Each test runs against a fresh database โ€” Respawn resets the data between tests without recreating the schema.


Running the Tests

dotnet test

NotoriousTest handles the full lifecycle:

Session starts
  โ””โ”€ SQL Server container starts (Testcontainers)
  โ””โ”€ Database "NotoriousDb_{id}" is created
  โ””โ”€ Users table is created
  โ””โ”€ Web application starts, connection string injected

Before each test
  โ””โ”€ Respawn empties all tables

Each test runs in isolation

Session ends
  โ””โ”€ Container is stopped and removed

๐Ÿ’ก Need help or have feedback? Join the community discussions or open an issue on GitHub.