๐ 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
ConfigurationEntryunderConnectionStrings: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:
- Resolves
SqlServerInfrastructurevia the internal DI container (all constructor parameters injected automatically). - Initializes infrastructures in order โ
SqlServerInfrastructurefirst (produces config),WebApplicationInfrastructurelast (consumes it). - 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.