This guide explains two different approaches for setting up integration tests with Microcks and Testcontainers in .NET, each with their own trade-offs and use cases.
When writing integration tests that use Microcks and Kafka containers, you have two main architectural choices:
- IClassFixture Pattern: Multiple container instances, isolated per test class
- ICollectionFixture Pattern: A set of shared container instances (e.g., one Microcks container and one Kafka container) used by all test classes for optimal performance and resource efficiency
- When you need complete isolation between test classes
- When different test classes require different container configurations
- When you have few test classes and startup time is not a concern
- When test classes might interfere with each other's state
public class MyTestClass : IClassFixture<OrderServiceWebApplicationFactory<Program>>
{
// Each test class gets its own factory instance
// Each factory starts its own containers
}- Dynamic Port Allocation: Each factory instance must use different ports
- Container Isolation: Each test class has its own Microcks and Kafka containers
- Resource Management: More memory and CPU usage due to multiple containers
Note: The implementation of
OrderServiceWebApplicationFactory(orOrderServiceWebApplicationFactory) must be adapted depending on the fixture pattern:
With IClassFixture, each test class gets its own factory and containers. You must allocate dynamic ports for Kestrel, Kafka, and all services, because static port mapping (like
kafka:9092:9092) is not possible—otherwise, you will have port conflicts if tests run in parallel. All port assignments must be programmatic and injected into your test server and mocks.With ICollectionFixture (shared collection), a single factory and set of containers are shared for all tests. You allocate ports only once, which simplifies configuration and avoids conflicts. This is why the shared collection pattern is recommended for most test suites.
public class OrderServiceWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram>, IAsyncLifetime
where TProgram : class
{
public ushort ActualPort { get; private set; }
public KafkaContainer KafkaContainer { get; private set; } = null!;
public MicrocksContainerEnsemble MicrocksContainerEnsemble { get; private set; } = null!;
private ushort GetAvailablePort()
{
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Bind(new IPEndPoint(IPAddress.Any, 0));
return (ushort)((IPEndPoint)socket.LocalEndPoint!).Port;
}
public async ValueTask InitializeAsync()
{
// CRITICAL: Get dynamic port for each instance
ActualPort = GetAvailablePort();
UseKestrel(ActualPort);
await TestcontainersSettings.ExposeHostPortsAsync(ActualPort, TestContext.Current.CancellationToken);
var network = new NetworkBuilder().Build();
// Each instance gets its own Kafka container
KafkaContainer = new KafkaBuilder()
.WithImage("confluentinc/cp-kafka:7.9.0")
.WithPortBinding(0, KafkaBuilder.KafkaPort) // 0 = dynamic port
.WithNetwork(network)
.WithNetworkAliases("kafka")
.Build();
await KafkaContainer.StartAsync(TestContext.Current.CancellationToken);
// Each instance gets its own Microcks container
MicrocksContainerEnsemble = new MicrocksContainerEnsemble(network, "quay.io/microcks/microcks-uber:1.13.0")
.WithAsyncFeature()
.WithMainArtifacts("resources/order-service-openapi.yaml")
.WithKafkaConnection(new KafkaConnection($"kafka:19092"));
await MicrocksContainerEnsemble.StartAsync();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
var pastryApiEndpoint = MicrocksContainerEnsemble.MicrocksContainer
.GetRestMockEndpoint("API Pastries", "0.0.1");
builder.UseSetting("PastryApi:BaseUrl", pastryApiEndpoint);
var kafkaBootstrap = KafkaContainer.GetBootstrapAddress()
.Replace("PLAINTEXT://", "", StringComparison.OrdinalIgnoreCase);
builder.UseSetting("Kafka:BootstrapServers", kafkaBootstrap);
}
public async override ValueTask DisposeAsync()
{
await base.DisposeAsync();
await KafkaContainer.DisposeAsync();
await MicrocksContainerEnsemble.DisposeAsync();
}
}public class OrderControllerTests : IClassFixture<OrderServiceWebApplicationFactory<Program>>
{
private readonly OrderServiceWebApplicationFactory<Program> _factory;
private readonly ITestOutputHelper _testOutput;
public OrderControllerTests(
OrderServiceWebApplicationFactory<Program> factory,
ITestOutputHelper testOutput)
{
_factory = factory;
_testOutput = testOutput;
}
[Fact]
public async Task CreateOrder_ShouldReturnCreatedOrder()
{
// This test class has its own containers
using var client = _factory.CreateClient();
// Test implementation...
}
}✅ Advantages:
- Complete isolation between test classes
- Different configurations per test class
- No shared state issues
- Parallel test execution per class
❌ Disadvantages:
- Higher resource usage (multiple containers)
- Slower overall test execution
- More complex port management
- Potential for port conflicts if not handled properly
- When you want optimal performance and resource usage
- When test classes can share the same container configuration
- When you have many test classes
- When startup time is a concern
[Collection(SharedTestCollection.Name)]
public class MyTestClass
{
// All test classes share the same factory instance
// Single set of containers for all tests
}- Single set of containers Instances: One Microcks + one Kafka container for all tests
- Performance Optimized: ~70% faster test execution
- Resource Efficient: Lower memory and CPU usage
- Single Port Allocation: One Kestrel port for the entire test suite
[CollectionDefinition(Name)]
public class SharedTestCollection : ICollectionFixture<OrderServiceWebApplicationFactory<Program>>
{
public const string Name = "SharedTestCollection";
}public class OrderServiceWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram>, IAsyncLifetime
where TProgram : class
{
private static readonly SemaphoreSlim InitializationSemaphore = new(1, 1);
private static bool _isInitialized;
public ushort ActualPort { get; private set; }
public KafkaContainer KafkaContainer { get; private set; } = null!;
public MicrocksContainerEnsemble MicrocksContainerEnsemble { get; private set; } = null!;
public async ValueTask InitializeAsync()
{
await InitializationSemaphore.WaitAsync();
try
{
if (_isInitialized)
{
TestLogger.WriteLine("[Factory] Already initialized, skipping...");
return;
}
TestLogger.WriteLine("[Factory] Starting initialization...");
// Single port allocation for all tests
ActualPort = GetAvailablePort();
UseKestrel(ActualPort);
await TestcontainersSettings.ExposeHostPortsAsync(ActualPort, TestContext.Current.CancellationToken);
// Single network and containers for all tests
var network = new NetworkBuilder().Build();
KafkaContainer = new KafkaBuilder()
.WithImage("confluentinc/cp-kafka:7.9.0")
.WithNetwork(network)
.WithNetworkAliases("kafka")
.Build();
await KafkaContainer.StartAsync(TestContext.Current.CancellationToken);
MicrocksContainerEnsemble = new MicrocksContainerEnsemble(network, "quay.io/microcks/microcks-uber:1.13.0")
.WithAsyncFeature()
.WithMainArtifacts("resources/order-service-openapi.yaml")
.WithKafkaConnection(new KafkaConnection("kafka:19092"));
await MicrocksContainerEnsemble.StartAsync();
_isInitialized = true;
TestLogger.WriteLine("[Factory] Initialization completed");
}
finally
{
InitializationSemaphore.Release();
}
}
// ConfigureWebHost and DisposeAsync similar to Pattern 1
}[Collection(SharedTestCollection.Name)]
public abstract class BaseIntegrationTest
{
public WebApplicationFactory<Program> Factory { get; private set; }
public ushort Port { get; private set; }
public MicrocksContainerEnsemble MicrocksContainerEnsemble { get; }
public KafkaContainer KafkaContainer { get; }
public HttpClient HttpClient { get; private set; }
protected BaseIntegrationTest(OrderServiceWebApplicationFactory<Program> factory)
{
Factory = factory;
HttpClient = factory.CreateClient();
Port = factory.ActualPort;
MicrocksContainerEnsemble = factory.MicrocksContainerEnsemble;
KafkaContainer = factory.KafkaContainer;
}
protected void SetupTestOutput(ITestOutputHelper testOutputHelper)
{
TestLogger.SetTestOutput(testOutputHelper);
}
}public class OrderControllerTests : BaseIntegrationTest
{
private readonly ITestOutputHelper _testOutput;
public OrderControllerTests(
ITestOutputHelper testOutput,
OrderServiceWebApplicationFactory<Program> factory)
: base(factory)
{
_testOutput = testOutput;
SetupTestOutput(testOutput);
}
[Fact]
public async Task CreateOrder_ShouldReturnCreatedOrder()
{
// Shared containers with all other test classes
// Test implementation...
}
}✅ Advantages:
- Excellent performance (~70% faster)
- Lower resource usage
- Simple port management
- No port conflicts
- Shared infrastructure
❌ Disadvantages:
- Shared state between test classes
- Same configuration for all tests
- Potential for test interdependencies
| Aspect | IClassFixture Pattern | ICollectionFixture Pattern |
|---|---|---|
| Performance | Slower (multiple startups) | Faster (~70% improvement) |
| Resource Usage | High (multiple containers) | Low (single containers) |
| Isolation | Complete per class | Shared across classes |
| Port Management | Complex (dynamic per class) | Simple (single allocation) |
| Configuration | Flexible per class | Single configuration |
| Recommended For | Different configs needed | Homogeneous test suites |
Use ICollectionFixture Pattern (Pattern 2) for most scenarios because:
- Better performance and resource efficiency
- Simpler port management
- Most integration tests can share the same container setup
- Easier to maintain and debug
Use IClassFixture Pattern (Pattern 1) only when:
- You need different container configurations per test class
- Complete isolation is mandatory
- You have few test classes and performance isn't critical