Skip to content

Testing with xUnit.net

The Testcontainers.Xunit package simplifies writing tests with containers in xUnit.net. By leveraging xUnit.net's shared context, this package automates the setup and teardown of test resources, creating and disposing of containers as needed. This reduces repetitive code and avoids common patterns that developers would otherwise need to implement repeatedly.

To get started, add the following dependency to your project file:

NuGet
1
dotnet add package Testcontainers.Xunit

Creating an isolated test context

To create a new test resource instance for each test, inherit from the ContainerTest<TBuilderEntity, TContainerEntity> class. Each test resource instance is isolated and not shared across other tests, making this approach ideal for destructive operations that could interfere with other tests. You can access the generic TContainerEntity container instance through the Container property.

The example below demonstrates how to override the Configure(TBuilderEntity) method and pin the image version. This method allows you to configure the container instance specifically for your test case, with all container builder methods available. If your tests rely on a Testcontainers' module, the module's default configurations will be applied.

1
2
3
4
5
6
7
8
9
public sealed partial class RedisContainerTest(ITestOutputHelper testOutputHelper)
    : ContainerTest<RedisBuilder, RedisContainer>(testOutputHelper)
{
    protected override RedisBuilder Configure(RedisBuilder builder)
    {
        // šŸ‘‡ Configure your container instance here.
        return builder.WithImage("redis:7.0");
    }
}

Tip

Always pin the image version to avoid flakiness. This ensures consistency and prevents unexpected behavior, as the latest tag may pointing to a new version.

The base class also receives an instance of xUnit.net's ITestOutputHelper to capture and forward log messages to the running test.

Considering that xUnit.net runs tests in a deterministic natural sort order (like Test1, Test2, etc.), retrieving the Redis (string) value in the second test will always return null since a new test resource instance (Redis container) is created for each test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[Fact]
public async Task Test1()
{
    // šŸ‘† A new container instance is created and started before this method (test) runs.
    using var redis = await ConnectionMultiplexer.ConnectAsync(Container.GetConnectionString());
    await redis.GetDatabase().StringSetAsync("key", "value");
    Assert.True(redis.IsConnected);
    // šŸ‘‡ The created and started container is disposed of after this method (test) completes.
}

[Fact]
public async Task Test2()
{
    // šŸ‘† A new container instance is created and started before this method (test) runs.
    using var redis = await ConnectionMultiplexer.ConnectAsync(Container.GetConnectionString());
    var redisValue = await redis.GetDatabase().StringGetAsync("key");
    Assert.True(redisValue.IsNull);
    // šŸ‘‡ The created and started container is disposed of after this method (test) completes.
}

If you check the output of docker ps, you will notice that three container instances in total are run, with two of them being Redis instances.

List running containers
1
2
3
4
5
PS C:\Sources\dotnet\testcontainers-dotnet> docker ps
CONTAINER ID   IMAGE                       COMMAND                  CREATED
be115f3df138   redis:7.0                   "docker-entrypoint.sā€¦"   3 seconds ago
59349127f8c0   redis:7.0                   "docker-entrypoint.sā€¦"   4 seconds ago
45fa02b3e997   testcontainers/ryuk:0.9.0   "/bin/ryuk"              4 seconds ago

Creating a shared test context

Sometimes, creating and disposing of a test resource can be an expensive operation that you do not want to repeat for every test. By inheriting from the ContainerFixture<TBuilderEntity, TContainerEntity> class, you can share the test resource instance across all tests within the same test class.

xUnit.net's fixture implementation does not rely on the ITestOutputHelper interface to capture and forward log messages; instead, it expects an implementation of IMessageSink. Make sure your fixture's default constructor accepts the interface implementation and forwards it to the base class.

1
2
3
4
5
6
7
8
9
[UsedImplicitly]
public sealed class RedisContainerFixture(IMessageSink messageSink)
    : ContainerFixture<RedisBuilder, RedisContainer>(messageSink)
{
    protected override RedisBuilder Configure(RedisBuilder builder)
    {
        return builder.WithImage("redis:7.0");
    }
}

This ensures that the fixture is created only once for the entire test class, which also improves overall test performance. You must implement the IClassFixture<TFixture> interface with the previously created container fixture type in your test class and add the type as argument to the default constructor.

1
2
public sealed partial class RedisContainerTest(RedisContainerFixture fixture)
    : IClassFixture<RedisContainerFixture>;

In this case, retrieving the Redis (string) value in the second test will no longer return null. Instead, it will return the value added in the first test.

1
2
3
4
5
6
7
[Fact]
public async Task Test2()
{
    using var redis = await ConnectionMultiplexer.ConnectAsync(fixture.Container.GetConnectionString());
    var redisValue = await redis.GetDatabase().StringGetAsync("key");
    Assert.Equal("value", redisValue);
}

The output of docker ps shows that, instead of two Redis containers, only one runs.

List running containers
1
2
3
4
PS C:\Sources\dotnet\testcontainers-dotnet> docker ps
CONTAINER ID   IMAGE                       COMMAND                  CREATED
d29a393816ce   redis:7.0                   "docker-entrypoint.sā€¦"   3 seconds ago
e878f0b8f4bc   testcontainers/ryuk:0.9.0   "/bin/ryuk"              3 seconds ago

Testing ADO.NET services

In addition to the two mentioned base classes, the package contains two more classes: DbContainerTest and DbContainerFixture, which behave identically but offer additional convenient features when working with services accessible through an ADO.NET provider.

Inherit from either the DbContainerTest or DbContainerFixture class and override the Configure(TBuilderEntity) method to configure your database service.

In this example, we use the default configuration of the PostgreSQL module. The container image capabilities are used to instantiate the database, schema, and test data. During startup, the PostgreSQL container runs SQL scripts placed under the /docker-entrypoint-initdb.d/ directory automatically.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public sealed partial class PostgreSqlContainerTest(ITestOutputHelper testOutputHelper)
    : DbContainerTest<PostgreSqlBuilder, PostgreSqlContainer>(testOutputHelper)
{
    protected override PostgreSqlBuilder Configure(PostgreSqlBuilder builder)
    {
        return builder
            .WithImage("postgres:15.1")
            .WithResourceMapping("Chinook_PostgreSql_AutoIncrementPKs.sql", "/docker-entrypoint-initdb.d/");
    }
}

Inheriting from the database container test or fixture class requires you to implement the abstract DbProviderFactory property and resolve a compatible DbProviderFactory according to your ADO.NET service.

1
public override DbProviderFactory DbProviderFactory => NpgsqlFactory.Instance;

Note

Depending on how you initialize and access the database, it may be necessary to override the ConnectionString property and replace the default database name with the one actual in use.

After configuring the dependent ADO.NET service, you can add the necessary tests. In this case, we run an SQL SELECT statement to retrieve the first record from the album table.

1
2
3
4
5
6
7
8
[Fact]
public async Task Test1()
{
    const string sql = "SELECT title FROM album ORDER BY album_id";
    using var connection = await OpenConnectionAsync();
    var title = await connection.QueryFirstAsync<string>(sql);
    Assert.Equal("For Those About To Rock We Salute You", title);
}

Tip

For the complete source code of this example and additional information, please refer to our test projects.