xUnit Adapter
Package: FluentDocker.Testing.Xunit
The xUnit adapter offers three patterns, from simplest to most flexible:
| Pattern | Lifecycle | Best for |
|---|---|---|
| Test base | Per-test (fresh container each test) | Isolated tests |
| Fixture base | Per-class or per-collection (shared) | Integration suites |
| Concrete fixture | Manual init (programmatic control) | Dynamic config |
Step by Step
- Basics: Test Bases (Per-Test Lifecycle), Fixture Bases (Shared Lifecycle)
- Intermediate: Collection Fixtures, Lifecycle Hooks with Wait Strategies
- Advanced: Concrete Fixtures (Advanced), Choosing the Right Pattern
Test Bases (Per-Test Lifecycle)
Inherit from an abstract test base. xUnit calls InitializeAsync before
each test and DisposeAsync after. Override ConfigureContainer (or
ConfigureCompose, ConfigureTopology) to provide your setup.
Container
public class RedisTests : XunitContainerTestBase
{
protected override void ConfigureContainer(IContainerBuilder b) =>
b.UseImage("redis:alpine")
.WaitForPort("6379/tcp");
[Fact]
public async Task Redis_IsRunning()
{
var info = await Resource.InspectAsync();
Assert.True(info.State.Running);
}
[Fact]
public async Task Can_Read_Logs()
{
var logs = await Resource.GetLogsAsync();
Assert.Contains("Ready to accept connections", logs);
}
}
Available properties: Resource (ContainerResource), Container
(IContainerService), Kernel (FluentDockerKernel).
Compose
public class AppTests : XunitComposeTestBase
{
protected override void ConfigureCompose(IComposeBuilder b) =>
b.WithComposeFile("docker-compose.yml")
.WithProjectName("app-tests");
[Fact]
public void Service_IsAvailable() => Assert.NotNull(Service);
}
Topology
public class MultiContainerTests : XunitTopologyTestBase
{
protected override void ConfigureTopology(Builder b)
{
b.UseNetwork(n => n.WithName("test-net"));
b.UseContainer(c => c
.UseImage("redis:alpine")
.WithNetwork("test-net"));
b.UseContainer(c => c
.UseImage("nginx:alpine")
.WithNetwork("test-net"));
}
[Fact]
public void Both_Containers_Running() =>
Assert.Equal(2, Resource.Containers.Count);
}
Custom Options and Kernel
Override GetOptions() or KernelFactory to customize:
public class PodmanRedisTests : XunitContainerTestBase
{
protected override void ConfigureContainer(IContainerBuilder b) =>
b.UseImage("redis:alpine");
protected override DockerResourceOptions GetOptions() => new()
{
Driver = DriverSelection.PodmanCli(),
InitializationTimeout = TimeSpan.FromMinutes(5)
};
protected override Func<Task<FluentDockerKernel>> KernelFactory =>
() => FluentDockerKernel.Create()
.WithPodmanCli("podman", d => d.AsDefault())
.BuildAsync();
}
Fixture Bases (Shared Lifecycle)
Inherit from an abstract fixture base and use it with IClassFixture<T> or
ICollectionFixture<T>. xUnit creates one instance and calls
InitializeAsync / DisposeAsync automatically via IAsyncLifetime – no
sync-over-async GetAwaiter().GetResult() needed.
Container Fixture
// 1. Define the fixture
public class RedisFixture : XunitContainerFixtureBase
{
protected override void ConfigureContainer(IContainerBuilder b) =>
b.UseImage("redis:alpine").WaitForPort("6379/tcp");
}
// 2. Use it in tests (shared across all tests in this class)
public class RedisTests : IClassFixture<RedisFixture>
{
private readonly RedisFixture _f;
public RedisTests(RedisFixture f) => _f = f;
[Fact]
public async Task Redis_IsRunning()
{
var info = await _f.Resource.InspectAsync();
Assert.True(info.State.Running);
}
[Fact]
public async Task Redis_IsListening()
{
Assert.NotNull(_f.Container);
}
}
Compose Fixture
public class AppFixture : XunitComposeFixtureBase
{
protected override void ConfigureCompose(IComposeBuilder b) =>
b.WithComposeFile("docker-compose.yml")
.WithProjectName("integration");
}
public class AppTests : IClassFixture<AppFixture>
{
private readonly AppFixture _f;
public AppTests(AppFixture f) => _f = f;
[Fact]
public void Service_IsAvailable() => Assert.NotNull(_f.Service);
}
Topology Fixture
public class StackFixture : XunitTopologyFixtureBase
{
protected override void ConfigureTopology(Builder b)
{
b.UseContainer(c => c.UseImage("redis:alpine"));
b.UseContainer(c => c.UseImage("nginx:alpine"));
}
}
public class StackTests : IClassFixture<StackFixture>
{
private readonly StackFixture _f;
public StackTests(StackFixture f) => _f = f;
[Fact]
public void All_Containers_Running() =>
Assert.Equal(2, _f.Resource.Containers.Count);
}
Collection Fixtures
Share a fixture across multiple test classes by defining a collection:
[CollectionDefinition("Redis")]
public class RedisCollection : ICollectionFixture<RedisFixture> { }
[Collection("Redis")]
public class RedisWriteTests
{
private readonly RedisFixture _f;
public RedisWriteTests(RedisFixture f) => _f = f;
// Tests share the same container as RedisReadTests
}
[Collection("Redis")]
public class RedisReadTests
{
private readonly RedisFixture _f;
public RedisReadTests(RedisFixture f) => _f = f;
}
Concrete Fixtures (Advanced)
Use concrete fixtures when you need programmatic control over initialization – e.g., dynamic configuration, conditional setup, or runtime-computed parameters.
Using Configure (Recommended)
Call Configure(...) in your constructor. xUnit calls IAsyncLifetime
automatically – no sync-over-async needed:
public class DynamicFixture : XunitContainerFixture
{
public DynamicFixture()
{
Configure(builder => builder
.UseImage(Environment.GetEnvironmentVariable("TEST_IMAGE")
?? "redis:alpine")
.WaitForPort("6379/tcp"));
}
}
public class RedisTests : IClassFixture<DynamicFixture>
{
private readonly DynamicFixture _f;
public RedisTests(DynamicFixture f) => _f = f;
[Fact]
public void Container_IsRunning() => Assert.NotNull(_f.Container);
}
XunitResourceFixture<TResource>
Generic fixture for any ITestResource, including plugin resources:
public class CustomFixture : XunitResourceFixture<ContainerResource>
{
public CustomFixture()
{
Configure(kernel =>
new ContainerResource(kernel,
c => c.UseImage("redis:alpine")));
}
}
Manual InitializeAsync (Legacy)
For backward compatibility, you can still call InitializeAsync directly:
public class ManualFixture : XunitContainerFixture
{
public ManualFixture()
{
InitializeAsync(builder => builder
.UseImage("redis:alpine")
).GetAwaiter().GetResult();
}
}
Other Concrete Fixtures
XunitComposeFixture– Docker ComposeXunitTopologyFixture– Multi-container topologyXunitSwarmStackFixture– Docker Swarm stacksXunitPodmanKubernetesFixture– Podmankube play
All support Configure(...) with optional kernelFactory and
DockerResourceOptions.
Lifecycle Hooks with Wait Strategies
Hooks on ResourceBase run at specific lifecycle points. The most common
use case is OnAfterReady for custom wait strategies beyond what the
builder’s built-in wait conditions provide.
Wait for a Health Check
public class PostgresTests : XunitContainerTestBase
{
protected override void ConfigureContainer(IContainerBuilder b) =>
b.UseImage("postgres:16")
.WithEnvironment("POSTGRES_PASSWORD=test")
.WaitForHealthy(60_000); // Uses Docker HEALTHCHECK
}
Wait for a Log Message
public class PostgresTests : XunitContainerTestBase
{
protected override void ConfigureContainer(IContainerBuilder b) =>
b.UseImage("postgres:16")
.WithEnvironment("POSTGRES_PASSWORD=test")
.WaitForLogMessage("ready to accept connections", 60_000);
}
Wait for an HTTP Endpoint
public class ApiTests : XunitContainerTestBase
{
protected override void ConfigureContainer(IContainerBuilder b) =>
b.UseImage("my-api:latest")
.ExposePort("8080")
.WaitForHttp("8080/tcp", "/health", 30_000);
}
Custom Wait via OnAfterReady Hook
For wait strategies not covered by the built-in conditions (e.g., database connectivity, custom protocol checks), use hooks on the resource directly. Create the resource manually and attach hooks before initialization:
// MSTest / NUnit style — works with any framework
var (kernel, resource) = await ResourceLifecycle.CreateAndInitializeAsync(
k =>
{
var r = new ContainerResource(k,
b => b.UseImage("postgres:16")
.WithEnvironment("POSTGRES_PASSWORD=test")
.ExposePort("5432"));
r.OnAfterReady(async _ =>
{
// Poll until Postgres accepts connections
var endpoint = r.Container.ToHostExposedEndpoint("5432/tcp");
var connStr = $"Host=localhost;Port={endpoint.Port};" +
"Username=postgres;Password=test";
for (var i = 0; i < 30; i++)
{
try
{
await using var conn = new NpgsqlConnection(connStr);
await conn.OpenAsync();
return; // Connected!
}
catch { await Task.Delay(1000); }
}
throw new TimeoutException("Postgres not ready after 30s");
});
return r;
});
All Lifecycle Hooks
resource
.OnBeforeInitialize(async r => { /* before preflight + provisioning */ })
.OnAfterReady(async r => { /* resource is up — verify readiness */ })
.OnBeforeDispose(async r => { /* before teardown — flush data, etc */ })
.OnAfterDispose(async r => { /* after cleanup — log final state */ });
Hooks are chainable and run in registration order. Init-phase hooks that throw will abort initialization with diagnostics captured. Dispose-phase hooks are best-effort (exceptions are suppressed to ensure cleanup proceeds).
Choosing the Right Pattern
| Need | Use |
|---|---|
| Fresh container per test | XunitContainerTestBase |
| Shared container for one test class | XunitContainerFixtureBase + IClassFixture<T> |
| Shared container across test classes | XunitContainerFixtureBase + ICollectionFixture<T> |
| Dynamic/runtime configuration | XunitContainerFixture (concrete) |
| Custom resource type or plugin | XunitResourceFixture<TResource> |
| Full kernel control | IAsyncLifetime + FluentDockerKernel directly |