Test Migration Guide
How to migrate FluentDocker v2.x.x test code to v3.0.0.
Note: The legacy
Ductus.FluentDocker.MsTestandDuctus.FluentDocker.XUnitpackages have been removed. The examples below show the builder-level API changes. For test support, use the newFluentDocker.Testing.*packages. See Migration from Legacy for side-by-side adapter examples.
This guide covers the most common test patterns and shows side-by-side v2 vs v3
code for each. The core change is that v3 requires a kernel with a registered
driver, the builder uses lambda-scoped configuration, and Build() returns
a BuildResults object instead of a service directly.
1. xUnit with IAsyncLifetime
The most common pattern: a test class that spins up a container before tests run and tears it down afterward.
v2
using Ductus.FluentDocker.Builders;
using Ductus.FluentDocker.Services;
using Xunit;
public class PostgresTests : IAsyncLifetime
{
private IContainerService _container;
public async Task InitializeAsync()
{
_container = new Builder()
.UseContainer()
.UseImage("postgres:15-alpine")
.WithEnvironment("POSTGRES_PASSWORD=test")
.ExposePort(5432)
.WaitForPort("5432/tcp", 30000)
.Build()
.Start();
}
public Task DisposeAsync()
{
_container?.Dispose();
return Task.CompletedTask;
}
[Fact]
public void Container_IsRunning()
{
Assert.Equal(ServiceRunningState.Running, _container.State);
}
}
v3
using FluentDocker.Builders;
using FluentDocker.Kernel;
using FluentDocker.Services;
using FluentDocker.Services.Extensions;
using Xunit;
public class PostgresTests : IAsyncLifetime
{
private FluentDockerKernel _kernel;
private BuildResults _results;
private IContainerService _container;
public async ValueTask InitializeAsync()
{
_kernel = await FluentDockerKernel.Create()
.WithDockerCli("docker", d => d.AsDefault())
.BuildAsync();
_results = await new Builder()
.WithinDriver("docker", _kernel)
.UseContainer(c => c
.UseImage("postgres:15-alpine")
.WithEnvironment("POSTGRES_PASSWORD=test")
.ExposePort("5432")
.WaitForPort("5432/tcp", 30000))
.BuildAsync();
_container = _results.Containers.First();
}
public async ValueTask DisposeAsync()
{
if (_results is IAsyncDisposable ad) await ad.DisposeAsync();
if (_kernel is IAsyncDisposable kd) await kd.DisposeAsync();
}
[Fact]
public void Container_IsRunning()
{
Assert.Equal(ServiceRunningState.Running, _container.State);
}
}
What changed:
- Three fields instead of one:
_kernel,_results,_container. FluentDockerKernel.Create()...BuildAsync()creates the kernel with a DockerCli driver.- Builder uses
.WithinDriver("docker", _kernel)to scope to the registered driver. - Container configuration is inside a lambda:
.UseContainer(c => c. ...). Build()no longer returns a service. Use_results.Containers.First().- No
.Start()call –Build()/BuildAsync()starts services automatically. DisposeAsyncusesIAsyncDisposablepattern; dispose results first, then kernel.- xUnit v3:
IAsyncLifetimemethods returnValueTaskinstead ofTask. - Port arguments are strings in v3 (e.g.,
"5432"not5432).
2. xUnit Collection Fixtures
Share one kernel and container across multiple test classes for speed. Multiple kernels per application are supported when you need stronger isolation.
v2
// v2 -- shared fixture
public class SharedDatabaseFixture : IDisposable
{
public IContainerService Container { get; }
public string ConnectionString { get; }
public SharedDatabaseFixture()
{
Container = new Builder()
.UseContainer()
.UseImage("postgres:15-alpine")
.WithEnvironment("POSTGRES_PASSWORD=test")
.ExposePort(5432)
.WaitForPort("5432/tcp", 30000)
.Build()
.Start();
var ep = Container.ToHostExposedEndpoint("5432/tcp");
ConnectionString =
$"Host=localhost;Port={ep.Port};Database=postgres;" +
"Username=postgres;Password=test";
}
public void Dispose() => Container?.Dispose();
}
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<SharedDatabaseFixture> { }
[Collection("Database")]
public class UserRepoTests
{
private readonly SharedDatabaseFixture _db;
public UserRepoTests(SharedDatabaseFixture db) => _db = db;
[Fact]
public void CanConnect() { /* use _db.ConnectionString */ }
}
v3
// v3 -- shared fixture with kernel
public class SharedDatabaseFixture : IAsyncLifetime
{
public FluentDockerKernel Kernel { get; private set; }
public IContainerService Container { get; private set; }
public string ConnectionString { get; private set; }
private BuildResults _results;
public async ValueTask InitializeAsync()
{
Kernel = await FluentDockerKernel.Create()
.WithDockerCli("docker", d => d.AsDefault())
.BuildAsync();
_results = await new Builder()
.WithinDriver("docker", Kernel)
.UseContainer(c => c
.UseImage("postgres:15-alpine")
.WithEnvironment("POSTGRES_PASSWORD=test")
.ExposePort("5432")
.WaitForPort("5432/tcp", 30000))
.BuildAsync();
Container = _results.Containers.First();
var ep = Container.ToHostExposedEndpoint("5432/tcp");
ConnectionString =
$"Host=localhost;Port={ep.Port};Database=postgres;" +
"Username=postgres;Password=test";
}
public async ValueTask DisposeAsync()
{
if (_results is IAsyncDisposable ad) await ad.DisposeAsync();
if (Kernel is IAsyncDisposable kd) await kd.DisposeAsync();
}
}
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<SharedDatabaseFixture> { }
[Collection("Database")]
public class UserRepoTests
{
private readonly SharedDatabaseFixture _db;
public UserRepoTests(SharedDatabaseFixture db) => _db = db;
[Fact]
public async Task CanConnect()
{
// Use _db.ConnectionString -- same container for all tests in collection
}
}
[Collection("Database")]
public class OrderRepoTests
{
private readonly SharedDatabaseFixture _db;
public OrderRepoTests(SharedDatabaseFixture db) => _db = db;
[Fact]
public async Task CanCreateOrder()
{
// Shares the same database container
}
}
What changed:
- Fixture implements
IAsyncLifetimeinstead ofIDisposable. - Kernel is created once in the fixture; all collection classes share it.
BuildResultsis stored for proper async disposal.- xUnit v3:
IAsyncLifetimemethods returnValueTaskinstead ofTask.
3. MSTest with MsTestResourceHelpers
The FluentDockerTestBase base class has been removed. Use
MsTestResourceHelpers static methods instead.
v2
using Ductus.FluentDocker.Builders;
using Ductus.FluentDocker.MsTest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class RedisTests : FluentDockerTestBase
{
protected override ContainerBuilder Build()
{
return new Builder()
.UseContainer()
.UseImage("redis:alpine")
.ExposePort(6379)
.WaitForPort("6379/tcp", 30000);
}
[TestMethod]
public void Container_IsRunning()
{
Assert.AreEqual(ServiceRunningState.Running, Container.State);
}
}
v3
using FluentDocker.Testing.MsTest;
using FluentDocker.Testing.Core;
using FluentDocker.Kernel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class RedisTests
{
private static FluentDockerKernel _kernel;
private static ContainerResource _resource;
[ClassInitialize]
public static async Task ClassInit(TestContext context)
{
(_kernel, _resource) = await MsTestResourceHelpers.CreateContainerAsync(
builder => builder
.UseImage("redis:alpine")
.ExposePort("6379")
.WaitForPort("6379/tcp", 30000));
}
[ClassCleanup]
public static async Task ClassCleanup()
{
await MsTestResourceHelpers.DisposeAsync(_resource, _kernel);
}
[TestMethod]
public void Container_IsRunning()
{
Assert.IsNotNull(_resource.Container);
}
}
What changed:
- No base class – use
MsTestResourceHelpers.CreateContainerAsyncstatic method. - Namespace:
Ductus.FluentDocker.MsTesttoFluentDocker.Testing.MsTest. - Container configuration via lambda instead of
Build()override. - Cleanup via
MsTestResourceHelpers.DisposeAsync(resource, kernel). - Ports are strings.
Customising the Kernel (MSTest)
Pass a kernelFactory parameter for non-default configuration:
(_kernel, _resource) = await MsTestResourceHelpers.CreateContainerAsync(
builder => builder.UseImage("nginx:alpine").ExposePort("80"),
kernelFactory: async () => await FluentDockerKernel.Create()
.WithDockerCli("docker", d => d
.WithSudo(SudoMechanism.NoPassword).AsDefault())
.BuildAsync());
4. xUnit with XunitContainerFixture
The FluentDockerTestBase base class has been removed. Use
XunitContainerFixture instead. It implements IAsyncDisposable and manages
the kernel and resource lifecycle.
v2
using Ductus.FluentDocker.Builders;
using Ductus.FluentDocker.XUnit;
using Xunit;
public class NginxFixture : FluentDockerTestBase
{
protected override ContainerBuilder Build()
{
return new Builder()
.UseContainer()
.UseImage("nginx:alpine")
.ExposePort(80)
.WaitForPort("80/tcp", 30000);
}
}
public class NginxTests : IClassFixture<NginxFixture>
{
private readonly NginxFixture _fixture;
public NginxTests(NginxFixture fixture) => _fixture = fixture;
[Fact]
public void Container_IsRunning()
{
Assert.Equal(ServiceRunningState.Running, _fixture.Container.State);
}
}
v3
using FluentDocker.Testing.Xunit;
using FluentDocker.Services.Extensions;
using Xunit;
public class NginxFixture : XunitContainerFixture
{
public NginxFixture()
{
InitializeAsync(builder => builder
.UseImage("nginx:alpine")
.ExposePort("80")
.WaitForPort("80/tcp", 30000)
).GetAwaiter().GetResult();
}
}
public class NginxTests : IClassFixture<NginxFixture>
{
private readonly NginxFixture _fixture;
public NginxTests(NginxFixture fixture) => _fixture = fixture;
[Fact]
public void Container_IsRunning()
{
Assert.NotNull(_fixture.Container);
}
[Fact]
public async Task Nginx_ReturnsWelcomePage()
{
var endpoint = _fixture.Container.ToHostExposedEndpoint("80/tcp");
var client = new HttpClient();
var response = await client.GetStringAsync(
$"http://localhost:{endpoint.Port}");
Assert.Contains("Welcome to nginx", response);
}
}
What changed:
- Fixture inherits
XunitContainerFixtureinstead ofFluentDockerTestBase. - Namespace:
Ductus.FluentDocker.XUnittoFluentDocker.Testing.Xunit. - Container configuration via lambda in
InitializeAsyncinstead ofBuild()override. - Extension methods like
ToHostExposedEndpointrequireusing FluentDocker.Services.Extensions.
Tip: For new code, prefer the
Configure(...)pattern shown in docs/testing/xunit.md — avoids deadlock risk vs. sync-over-async in constructors.
5. Compose in Tests
Multi-container stacks use .UseCompose() instead of .UseContainer().UseCompose().
v2
using Ductus.FluentDocker.Builders;
using Ductus.FluentDocker.Services;
using Xunit;
public class ComposeTests : IAsyncLifetime
{
private ICompositeService _svc;
public async Task InitializeAsync()
{
_svc = new Builder()
.UseContainer()
.UseCompose()
.FromFile("docker-compose.yml")
.RemoveOrphans()
.WaitForHttp("api", "http://localhost:8080/health")
.Build()
.Start();
}
public Task DisposeAsync()
{
_svc?.Dispose();
return Task.CompletedTask;
}
[Fact]
public void AllServicesRunning()
{
Assert.Equal(ServiceRunningState.Running, _svc.State);
}
}
v3
using FluentDocker.Builders;
using FluentDocker.Kernel;
using FluentDocker.Services;
using FluentDocker.Services.Extensions;
using Xunit;
public class ComposeTests : IAsyncLifetime
{
private FluentDockerKernel _kernel;
private BuildResults _results;
public async ValueTask InitializeAsync()
{
_kernel = await FluentDockerKernel.Create()
.WithDockerCli("docker", d => d.AsDefault())
.BuildAsync();
_results = await new Builder()
.WithinDriver("docker", _kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithRemoveOrphans()
.WithWait()
.WithWaitTimeout(60))
.BuildAsync();
}
public async ValueTask DisposeAsync()
{
if (_results is IAsyncDisposable ad) await ad.DisposeAsync();
if (_kernel is IAsyncDisposable kd) await kd.DisposeAsync();
}
[Fact]
public void AllContainersAreAccessible()
{
Assert.NotEmpty(_results.Containers);
}
[Fact]
public async Task Api_RespondsToHealthCheck()
{
var api = _results.Containers
.First(c => c.Name.Contains("api"));
var endpoint = api.ToHostExposedEndpoint("8080/tcp");
var client = new HttpClient();
var response = await client.GetAsync(
$"http://localhost:{endpoint.Port}/health");
Assert.True(response.IsSuccessStatusCode);
}
}
To share a compose stack across test classes, apply the same collection fixture
pattern from section 2 but use .UseCompose() instead of .UseContainer().
Key compose differences:
| Aspect | v2 | v3 |
|---|---|---|
| Entry point | .UseContainer().UseCompose() |
.UseCompose(c => ...) |
| Compose file | .FromFile("x.yml") |
.WithComposeFile("x.yml") |
| Orphan removal | .RemoveOrphans() |
.WithRemoveOrphans() |
| Wait strategy | .WaitForHttp("svc", "url") |
.WithWait() + compose healthchecks |
| Build result | ICompositeService |
BuildResults |
| Access containers | _svc.Containers |
_results.Containers |
6. Key Differences Summary
| Aspect | v2 | v3 |
|---|---|---|
| Kernel | Not needed | Required: FluentDockerKernel.Create().WithDockerCli(...) |
| Build result type | IContainerService directly |
BuildResults (access .Containers.First()) |
| Type for field | IContainerService / ICompositeService |
BuildResults (concrete class) |
Interface IBuildResults |
Does not exist | Does not exist – use BuildResults |
| Dispose | IDisposable |
IAsyncDisposable preferred |
xUnit IAsyncLifetime |
Returns Task |
Returns ValueTask (xUnit v3) |
| Test base class | FluentDockerTestBase |
XunitContainerFixture / MsTestResourceHelpers |
| Start call | .Build().Start() |
.Build() / .BuildAsync() auto-starts |
| Port args | int (e.g., 80) |
string (e.g., "80") |
| Namespace | Ductus.FluentDocker.* |
FluentDocker.* |
| Extension methods | Implicit | using FluentDocker.Services.Extensions; |
| Docker Machine | new Hosts().Discover() |
Removed – use Docker Contexts |
| Sudo config | SudoMechanism.NoPassword.SetSudo() |
.WithSudo(SudoMechanism.NoPassword) in kernel builder |
Common Migration Mistakes
-
Forgetting async disposal order. Always dispose
BuildResultsbefore the kernel. The results hold references to containers that need the kernel’s driver to clean up. -
Using
IBuildResultsas a type. There is no such interface. Use the concreteBuildResultsclass. -
Calling
.Start()after.Build(). In v3,Build()/BuildAsync()already starts the services. Calling.Start()again is harmless but unnecessary. -
Missing
using FluentDocker.Services.Extensions;. Extension methods likeToHostExposedEndpointandGetConfigurationmoved to this namespace. -
Using
container.Resume(). Renamed tocontainer.Start()in v3. -
Using
container.Logs(). Renamed toawait container.GetLogsAsync().
Next Steps
- Migration Guide – full API migration reference
- Testing – complete v3 test documentation
- Docker Compose – compose patterns and examples