← Back to all posts

Testcontainers in .NET

Testcontainers in .NET

Reading Time: 8 minutes

The Problem

Integration tests often fail for one of two reasons:

  • they run against shared infrastructure and become flaky
  • they use in-memory fakes and miss SQL Server-specific behavior

For data-heavy .NET applications, this creates a dangerous gap. Tests pass locally and in CI, but production fails because real SQL Server behavior around constraints, collations, transactions, or query translation was never exercised.

The Solution

Use Testcontainers to create an isolated SQL Server container per test run (or per test suite), then run integration tests against that real database.

This gives you:

  • production-like behavior in automated tests
  • repeatable, disposable infrastructure
  • no dependency on pre-provisioned shared SQL environments
  • easier CI setup because tests self-provision what they need

Description

1. Install the required NuGet packages

For this SQL Server integration testing setup, these current stable package versions (as of April 17, 2026) are:

  • Testcontainers.MsSql version 4.11.0
  • Testcontainers version 4.11.0
  • Microsoft.NET.Test.Sdk version 18.4.0
  • xunit version 2.9.3
  • xunit.runner.visualstudio version 3.1.5
  • Microsoft.AspNetCore.Mvc.Testing version 10.0.6 (if you are testing ASP.NET Core APIs end to end)
dotnet add package Testcontainers.MsSql --version 4.11.0
dotnet add package Testcontainers --version 4.11.0
dotnet add package Microsoft.NET.Test.Sdk --version 18.4.0
dotnet add package xunit --version 2.9.3
dotnet add package xunit.runner.visualstudio --version 3.1.5

2. Create a SQL Server container fixture

Use a shared fixture so the container is created once for a test collection and reused across tests.

using Testcontainers.MsSql;
using Xunit;

public sealed class SqlServerContainerFixture : IAsyncLifetime
{
    private readonly MsSqlContainer _container = new MsSqlBuilder()
        .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
        .Build();

    public string ConnectionString => _container.GetConnectionString();

    public async Task InitializeAsync()
    {
        await _container.StartAsync();
    }

    public async Task DisposeAsync()
    {
        await _container.DisposeAsync();
    }
}

[CollectionDefinition("sqlserver")]
public sealed class SqlServerCollection : ICollectionFixture<SqlServerContainerFixture>
{
}

3. Run integration tests against the containerized SQL Server

This example verifies database connectivity and a real SQL query.

using Microsoft.Data.SqlClient;
using Xunit;

[Collection("sqlserver")]
public sealed class SqlServerIntegrationTests
{
    private readonly SqlServerContainerFixture _fixture;

    public SqlServerIntegrationTests(SqlServerContainerFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task Should_execute_query_against_real_sql_server()
    {
        await using var connection = new SqlConnection(_fixture.ConnectionString);
        await connection.OpenAsync();

        await using var createCmd = connection.CreateCommand();
        createCmd.CommandText = """
            IF OBJECT_ID('dbo.Products', 'U') IS NULL
            CREATE TABLE dbo.Products
            (
                Id INT IDENTITY(1,1) PRIMARY KEY,
                Name NVARCHAR(200) NOT NULL
            );
            """;
        await createCmd.ExecuteNonQueryAsync();

        await using var insertCmd = connection.CreateCommand();
        insertCmd.CommandText = "INSERT INTO dbo.Products (Name) VALUES (@name);";
        insertCmd.Parameters.AddWithValue("@name", "Keyboard");
        await insertCmd.ExecuteNonQueryAsync();

        await using var countCmd = connection.CreateCommand();
        countCmd.CommandText = "SELECT COUNT(*) FROM dbo.Products;";
        var count = (int)(await countCmd.ExecuteScalarAsync()!);

        Assert.Equal(1, count);
    }
}

4. Wire it into ASP.NET Core integration tests (optional)

If you are testing HTTP endpoints, inject the container connection string into your app under test using WebApplicationFactory, then run API calls with HttpClient.

High-level flow:

  1. start SQL Server container in fixture
  2. override app configuration/DbContext in test host
  3. apply migrations for the test database
  4. execute API test requests

This catches real end-to-end issues across middleware, data access, and SQL behavior.

5. Practical tips for reliable CI runs

  • prefer one container per test collection to reduce startup overhead
  • reset schema/data between tests if test isolation requires it
  • keep container image version explicit to avoid surprise behavior changes
  • run tests in parallel only when isolation strategy supports it
  • ensure Docker is available on CI agents before test execution

Summary

Testcontainers in .NET is a strong pattern for SQL Server integration tests because it combines realism with repeatability:

  • real SQL Server engine behavior
  • disposable environment per run
  • fewer flaky tests caused by shared infrastructure
  • better confidence before deployment

For teams building data-centric .NET APIs, this approach closes the gap between test and production behavior without requiring manually managed test databases.

References