← Back to list

Integration Testing in .NET With Testcontainers

Learn integration testing in .NET with Testcontainers for PostgreSQL, Redis, Kafka, InfluxDB, Azure Service Bus, and Cosmos DB.

Hasala Darshana · 2026-05-29 11:12 · 1 claps · 13.8 min read
#dotnet #testcontainer #integration-testing #azure #docker
Open on Medium ↗
Wiki topics: MM · Multimodal & Generative Media ☁️ · DevOps & Cloud 🔭 · Astronomy & Space

Integration Testing in .NET with Testcontainers (PostgreSQL, Redis, Kafka, InfluxDB, Azure Service Bus, and Cosmos DB)

**Testcontainers is an open-source library that lets you run temporary Docker containers directly from your test code. Instead of setting up databases manually or relying on mocks, you can spin up real instances of PostgreSQL, Redis, Kafka, InfluxDB, and even Azure services like Service Bus or Cosmos DB** while your tests run. This ensures your integration tests hit actual systems in a repeatable environment. Testcontainers handles the entire lifecycle starting and stopping containers automatically so you get production-grade tests without the hassle of maintaining local infrastructure.

In this guide, I’m going to walk through how to wire up Testcontainers for a variety of real-world scenarios. We’ll start with the basics like PostgreSQL and Redis, then move into time-series data with InfluxDB and event streaming with Kafka. Finally, I’ll show you how to handle the trickier Azure services, specifically Service Bus and Cosmos DB, so you can stop relying on local emulators that don’t always behave like the cloud.

This diagram illustrates how Testcontainers works behind the scenes. Connect your test code with Docker to create, run, and clean up containers automatically during integration testing.

Advantages of Testcontainers

  1. Run tests against real services like PostgresSQL, Redis, Kafka, InfluxDB, CosmosDB, and Azure Service Bus instead of mocks. This gives you production-level confidence in your integration Testscontainers ensure the same environment every time, reducing “works on my machine” issues.
  2. Testcontainers automatically start and stop, so you don’t need manual scripts or local installations.
  3. Each test suite gets its own clean instance of the service, preventing data leakage between tests.
  4. It fits smoothly into CI/CD pipelines. Containers start up when needed and shut down afterward, making your integration tests easy to run across different environments.
  5. You don’t need to install or configure services on your machine Testcontainers handles that for you, so you can spend your time writing tests instead of managing infrastructure.

Let’s take a look at how to set up Testcontainers in a .NET project using PostgreSQL, Redis, Influx DB, Kafka, Azure Service Bus and Azure Cosmos DB. Here are the exact configurations you need to get these integration tests running smoothly.

Prerequisites

Before you start using Testcontainers in .NET, make sure you have the following ready.

  1. **.NET 10 SDK** installed on your machine. (I’m using the latest version to keep things current, but the code is backward compatible with .NET 8 LTS)
  2. **Docker Desktop (or another Docker engine) installed and running**.
  3. Set up a test framework like **xUnit** in your project to run the examples.

Step 01: Create a new .NET 10 project. (I am going with the latest .NET 10, but you can also create a .NET 8 project)

In Command line,

dotnet new xunit -n MyTestProject

(Use the dotnet new xunit command to scaffold a test project targeting the latest installed framework.)

Step 02: Add xUnit Dependencies

Ensure xUnit is installed and configured for your project.

  • The dotnet new xunit template already includes xUnit.
  • Verify xUnit and xunit.runner.visualstudio packages are listed in your .csproj file.

By default, the Visual Studio project template installs xunit (v2.9.3), which is now deprecated in favor of xunit.v3. You can still use the old v2 package if you want, but for this guide, I’m going to use xunit.v3 (v3.2.2).

Because the async lifecycle methods changed between versions, I’ve included setup blocks for both in the code examples. The newer xUnit v3 syntax is active by default. If you are still using v2, just uncomment the v2-specific InitializeAsync and DisposeAsync methods and comment out the v3 ones.

xunit 2.9.3(deprecated)

dotnet remove package xunit
dotnet add package xunit.v3 --version 3.2.2

Step 03: Install Testcontainers Package

# Move into the test project folder
cd MyTestProject
# Add Testcontainers NuGet package (version 4.12.0 for .NET 10)
dotnet add package Testcontainers --version 4.12.0

For a .NET 10 project, you’ll want to use the latest stable release of Testcontainers for .NET. The current recommended version is 4.12.0, which includes support for .NET 10. Earlier 3.x releases were designed for .NET 6–8 but starting with the 4.x line the library was updated to work with newer runtimes.

Now you’ve successfully created a .NET 10 test project and installed both xUnit and Testcontainers.

PostgreSQL Example

Before writing the code, ensure you have installed the necessary NuGet packages. Run the following commands in your terminal or use the Manage NuGet Packages window in Visual Studio.

dotnet add package Testcontainers.PostgreSql --version 4.12.0 
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 10.0.2

Let’s look at a practical example using Entity Framework Core to see how this works in action.

using Microsoft.EntityFrameworkCore;
using Testcontainers.PostgreSql;

namespace MyTestProject
{
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; } = string.Empty;
    }

    public class UserDbContext : DbContext
    {
        public UserDbContext(DbContextOptions<UserDbContext> options) : base(options) { }

        public DbSet<User> Users => Set<User>();
    }
    public class PostgresEfCoreTests : IAsyncLifetime
    {
        private readonly PostgreSqlContainer _postgres;
        private UserDbContext _dbContext = null!;

        public PostgresEfCoreTests()
        {
            _postgres = new PostgreSqlBuilder("postgres:15-alpine")
                .WithDatabase("testdb")
                .WithUsername("testuser")
                .WithPassword("testpass")
                .Build();
        }

        //This commented mthod isfor the old xUnit versions that do not support ValueTask. If you are using xUnit 2.4 or later, you can use the ValueTask version below for better performance.
        //public async Task InitializeAsync()
        //{
        //    // Start container
        //    await _postgres.StartAsync();

        //    // Configure EF Core with container connection string
        //    var options = new DbContextOptionsBuilder<UserDbContext>()
        //        .UseNpgsql(_postgres.GetConnectionString())
        //        .Options;

        //    _dbContext = new UserDbContext(options);

        //    // Ensure schema is created
        //    await _dbContext.Database.EnsureCreatedAsync();
        //}

        //public async Task DisposeAsync()
        //{
        //    await _postgres.DisposeAsync();
        //}

        public async ValueTask InitializeAsync()
        {
            // Start container
            await _postgres.StartAsync();

            // Configure EF Core with container connection string
            var options = new DbContextOptionsBuilder<UserDbContext>()
                .UseNpgsql(_postgres.GetConnectionString())
                .Options;

            _dbContext = new UserDbContext(options);

            // Ensure schema is created
            await _dbContext.Database.EnsureCreatedAsync();
        }

        public async ValueTask DisposeAsync()
        {
            await _postgres.DisposeAsync();
        }

        [Fact]
        public async ValueTask InsertAndQueryUser_ShouldReturnInsertedUser()
        {
            // Insert a user
            _dbContext.Users.Add(new User { Name = "Mark" });
            await _dbContext.SaveChangesAsync();

            // Query with LINQ
            var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Name == "Mark");

            Assert.NotNull(user);
            Assert.Equal("Mark", user!.Name);
        }

        [Fact]
        public async Task InsertMultipleUsers_ShouldIncreaseCount()
        {
            // Insert multiple users
            _dbContext.Users.AddRange(
                new User { Name = "Alice" },
                new User { Name = "Bob" }
            );
            await _dbContext.SaveChangesAsync();

            // Verify the count
            var count = await _dbContext.Users.CountAsync();
            Assert.Equal(2, count);
        }
    }
}

To run the test, either use the terminal command below or right-click inside the test file in Visual Studio and click Run Tests.

dotnet test

Redis Example

Before you start coding, make sure to install the following NuGet packages. You can use the terminal commands below or the NuGet Package Manager in Visual Studio.

dotnet add package StackExchange.Redis --version 2.13.17 
dotnet add package Testcontainers.Redis --version 4.12.0

Let’s look at a practical example to see how this actually works in code,

using StackExchange.Redis;
using Testcontainers.Redis;

namespace MyTestProject
{
    public class RedisTestExample : IAsyncLifetime
    {
        private readonly RedisContainer _redis;
        private IConnectionMultiplexer _connection = null!;

        public RedisTestExample()
        {
            _redis = new RedisBuilder("redis:7-alpine").Build();
        }

        //This commented mthod isfor the old xUnit versions that do not support ValueTask. If you are using xUnit 2.4 or later, you can use the ValueTask version below for better performance.
        //public async Task InitializeAsync()
        //{
        //    await _redis.StartAsync();
        //    _connection = await ConnectionMultiplexer.ConnectAsync(_redis.GetConnectionString());
        //}

        //public async Task DisposeAsync()
        //{
        //    _connection?.Dispose();
        //    await _redis.DisposeAsync();
        //}

        public async ValueTask InitializeAsync()
        {
            await _redis.StartAsync();
            _connection = await ConnectionMultiplexer.ConnectAsync(_redis.GetConnectionString());
        }

        public async ValueTask DisposeAsync()
        {
            _connection?.Dispose();
            await _redis.DisposeAsync();
        }

        [Fact]
        public async Task SetAndGetValue_ShouldReturnValue()
        {
            var db = _connection.GetDatabase();

            // Set a value
            await db.StringSetAsync("mykey", "myvalue");

            // Get the value
            var value = await db.StringGetAsync("mykey");

            Assert.True(value.HasValue);
            Assert.Equal("myvalue", value.ToString());
        }

        [Fact]
        public async Task IncrementCounter_ShouldIncreaseValue()
        {
            var db = _connection.GetDatabase();

            // Increment counter
            var result = await db.StringIncrementAsync("counter");
            Assert.Equal(1, result);

            result = await db.StringIncrementAsync("counter");
            Assert.Equal(2, result);
        }
    }
}

To run the test, either use the terminal command below or right-click inside the test file in Visual Studio and click Run Tests.

dotnet test

InfluxDB Example

Before we get into the code, you’ll need to install the Testcontainers module for InfluxDB and the official .NET client. You can grab these using the NuGet Package Manager in Visual Studio or by running the following commands in your terminal.

dotnet add package Testcontainers.InfluxDb --version 4.12.0 
dotnet add package InfluxDB.Client --version 5.0.0

Let’s look at a practical example to see how this actually works in code,

For this example, I am targeting InfluxDB 2.x and using Flux queries to fetch and verify data.

using InfluxDB.Client;
using InfluxDB.Client.Api.Domain;
using InfluxDB.Client.Writes;
using Testcontainers.InfluxDb;

namespace MyTestProject
{
    public class InfluxDbTestExample : IAsyncLifetime
    {
        private readonly InfluxDbContainer _influxDb;
        private InfluxDBClient _client = null!;

        public InfluxDbTestExample()
        {
            _influxDb = new InfluxDbBuilder("influxdb:2.7-alpine")
                .WithAdminToken("testtoken123")
                .WithOrganization("testorg")
                .WithBucket("testbucket")
                .Build();
        }

        //This commented mthod isfor the old xUnit versions that do not support ValueTask. If you are using xUnit 2.4 or later, you can use the ValueTask version below for better performance.
        //public async Task InitializeAsync()
        //{
        //    await _influxDb.StartAsync();
        //    var url = _influxDb.GetConnectionString();
        //    _client = new InfluxDBClient(url, "testtoken123");
        //}

        //public async Task DisposeAsync()
        //{
        //    _client?.Dispose();
        //    await _influxDb.DisposeAsync();
        //}

        public async ValueTask InitializeAsync()
        {
            await _influxDb.StartAsync();
            var url = _influxDb.GetConnectionString();
            _client = new InfluxDBClient(url, "testtoken123");
        }

        public async ValueTask DisposeAsync()
        {
            _client?.Dispose();
            await _influxDb.DisposeAsync();
        }

        [Fact]
        public async Task WriteAndQueryData_ShouldReturnData()
        {
            var writeApi = _client.GetWriteApiAsync();
            var point = PointData.Measurement("temperature")
                .Tag("location", "room1")
                .Field("value", 22.5)
                .Timestamp(DateTime.UtcNow, WritePrecision.Ns);

            await writeApi.WritePointAsync(point, "testbucket", "testorg");

            var queryApi = _client.GetQueryApi();
            var query = @"from(bucket: ""testbucket"") 
                |> range(start: -1h) 
                |> filter(fn: (r) => r._measurement == ""temperature"")";

            var result = await queryApi.QueryAsync(query, "testorg");
            Assert.NotEmpty(result);
        }

        [Fact]
        public async Task WriteMultiplePoints_ShouldStoreAll()
        {
            var writeApi = _client.GetWriteApiAsync();

            for (int i = 0; i < 5; i++)
            {
                var point = PointData.Measurement("cpu")
                    .Tag("host", "server1")
                    .Field("usage", 50 + i)
                    .Timestamp(DateTime.UtcNow, WritePrecision.Ns);
                await writeApi.WritePointAsync(point, "testbucket", "testorg");
            }

            var queryApi = _client.GetQueryApi();
            var query = @"from(bucket: ""testbucket"") 
                |> range(start: -1h) 
                |> filter(fn: (r) => r._measurement == ""cpu"")
                |> count()";

            var result = await queryApi.QueryAsync(query, "testorg");
            Assert.NotEmpty(result);
        }

        [Fact]
        public async Task QueryWithTagFilter_ShouldReturnFilteredData()
        {
            var writeApi = _client.GetWriteApiAsync();

            // Write points with different tags
            var point1 = PointData.Measurement("temperature")
                .Tag("location", "room1")
                .Field("value", 20.0)
                .Timestamp(DateTime.UtcNow, WritePrecision.Ns);

            var point2 = PointData.Measurement("temperature")
                .Tag("location", "room2")
                .Field("value", 25.0)
                .Timestamp(DateTime.UtcNow, WritePrecision.Ns);

            await writeApi.WritePointAsync(point1, "testbucket", "testorg");
            await writeApi.WritePointAsync(point2, "testbucket", "testorg");

            // Query with tag filter
            var queryApi = _client.GetQueryApi();
            var query = @"from(bucket: ""testbucket"") 
                |> range(start: -1h) 
                |> filter(fn: (r) => r._measurement == ""temperature"" and r.location == ""room1"")";

            var result = await queryApi.QueryAsync(query, "testorg");
            Assert.NotEmpty(result);
            Assert.Single(result);
        }    
    }
}

To check your results, use the Test Explorer in Visual Studio to run everything at once, or execute dotnet test from the project root for a quick CLI output.

Apache Kafka Example

Before you start coding the Kafka tests, you’ll need to install the Testcontainers Kafka module and the Confluent Kafka client. You can do this via the NuGet Package Manager in Visual Studio or by running these commands in your terminal.

dotnet add package Testcontainers.Kafka --version 4.12.0 
dotnet add package Confluent.Kafka --version 2.14.0

Let’s look at a practical example to see how this actually works in code,

using Confluent.Kafka;
using Testcontainers.Kafka;

namespace MyTestProject
{
    public class KafkaTestExample : IAsyncLifetime
    {
        private readonly KafkaContainer _kafka;
        private IProducer<string, string> _producer = null!;
        private IConsumer<string, string> _consumer = null!;

        public KafkaTestExample()
        {
            _kafka = new KafkaBuilder("confluentinc/cp-kafka:7.4.15").Build();
        }

        //This commented mthod isfor the old xUnit versions that do not support ValueTask. If you are using xUnit 2.4 or later, you can use the ValueTask version below for better performance.
        //public async Task InitializeAsync()
        //{
        //    await _kafka.StartAsync();

        //    var producerConfig = new ProducerConfig
        //    {
        //        BootstrapServers = _kafka.GetBootstrapAddress()
        //    };
        //    _producer = new ProducerBuilder<string, string>(producerConfig).Build();

        //    var consumerConfig = new ConsumerConfig
        //    {
        //        BootstrapServers = _kafka.GetBootstrapAddress(),
        //        GroupId = "test-group",
        //        AutoOffsetReset = AutoOffsetReset.Earliest
        //    };
        //    _consumer = new ConsumerBuilder<string, string>(consumerConfig).Build();
        //}

        //public async Task DisposeAsync()
        //{
        //    _producer?.Dispose();
        //    _consumer?.Dispose();
        //    await _kafka.DisposeAsync();
        //}

        public async ValueTask InitializeAsync()
        {
            await _kafka.StartAsync();

            var producerConfig = new ProducerConfig
            {
                BootstrapServers = _kafka.GetBootstrapAddress()
            };
            _producer = new ProducerBuilder<string, string>(producerConfig).Build();

            var consumerConfig = new ConsumerConfig
            {
                BootstrapServers = _kafka.GetBootstrapAddress(),
                GroupId = "test-group",
                AutoOffsetReset = AutoOffsetReset.Earliest
            };
            _consumer = new ConsumerBuilder<string, string>(consumerConfig).Build();
        }

        public async ValueTask DisposeAsync()
        {
            _producer?.Dispose();
            _consumer?.Dispose();
            await _kafka.DisposeAsync();
        }

        [Fact]
        public async Task ProduceAndConsume_ShouldReturnMessage()
        {
            const string topic = "test-topic";

            // Produce message
            await _producer.ProduceAsync(topic, new Message<string, string>
            {
                Key = "key1",
                Value = "Hello Kafka"
            });

            // Subscribe and consume after topic exists
            _consumer.Subscribe(topic);
            var consumeResult = _consumer.Consume(TimeSpan.FromSeconds(10));

            Assert.NotNull(consumeResult);
            Assert.Equal("key1", consumeResult.Message.Key);
            Assert.Equal("Hello Kafka", consumeResult.Message.Value);
        }

        [Fact]
        public async Task ProduceMultipleMessages_ShouldConsumeAll()
        {
            const string topic = "test-topic-multi";

            // Produce multiple messages
            for (int i = 0; i < 5; i++)
            {
                await _producer.ProduceAsync(topic, new Message<string, string>
                {
                    Key = $"key{i}",
                    Value = $"Message {i}"
                });
            }

            // Subscribe and consume all messages
            _consumer.Subscribe(topic);
            var messages = new List<string>();
            for (int i = 0; i < 5; i++)
            {
                var result = _consumer.Consume(TimeSpan.FromSeconds(10));
                messages.Add(result.Message.Value);
            }

            Assert.Equal(5, messages.Count);
        }
    }
}

To check your results, use the Test Explorer in Visual Studio to run everything at once, or execute dotnet test from the project root for a quick CLI output.

Azure Service Bus Example

Before writing the code, ensure you have installed the necessary NuGet packages. Run the following commands in your terminal or use the Manage NuGet Packages window in Visual Studio.

dotnet add package Azure.Messaging.ServiceBus --version 7.20.1 
dotnet add package Testcontainers.ServiceBus --version 4.12.0

To prevent test suite timeouts during initial execution, it is highly recommended to pull the target image manually prior to running the integration tests. This ensures that the download overhead does not impact test execution limits or CI/CD runner pipelines.

docker pull mcr.microsoft.com/azure-messaging/servicebus-emulator:2.0.0

By default, await _serviceBus.StartAsync() only verifies that the underlying Docker container is running. It does not guarantee that the internal emulator application and its dependent SQL instance are fully initialized and ready to accept requests.

To bridge this gap, you must implement a log-based wait strategy. This explicitly instructs Testcontainers to defer test execution until the internal services have completely started.

.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Emulator Service is Successfully Up!"))

Let’s look at a practical example to see how this actually works in code,

using Azure.Messaging.ServiceBus;
using Azure.Messaging.ServiceBus.Administration;
using DotNet.Testcontainers.Builders;
using Testcontainers.ServiceBus;

namespace MyTestProject
{
    public class AzureServiceBusTestExample : IAsyncLifetime
    {
        private readonly ServiceBusContainer _serviceBus;
        private ServiceBusClient _client = null!;
        private ServiceBusAdministrationClient _administrationClient = null!;

        public AzureServiceBusTestExample()
        {
            _serviceBus = new ServiceBusBuilder("mcr.microsoft.com/azure-messaging/servicebus-emulator:2.0.0")
                .WithAcceptLicenseAgreement(true)
                .WithPortBinding(5672, true) // AMQP data port
                .WithPortBinding(5300, true) // HTTP management port
                // This tells Testcontainers to wait until the emulator says it's ready in the logs
                .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Emulator Service is Successfully Up!"))
                .Build();
        }

        public async ValueTask InitializeAsync()
        {
            await _serviceBus.StartAsync();

            var connectionString = _serviceBus.GetConnectionString();
            _client = new ServiceBusClient(connectionString);

            // Get the container's dynamically mapped host port for HTTP management (internal 5300)
            ushort adminPort = _serviceBus.GetMappedPublicPort(5300);

            // Build the special development emulator connection string for administration
            string adminConnectionString = $"Endpoint=sb://localhost:{adminPort};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KeyValue_placeholder;UseDevelopmentEmulator=true;";

            // Instantiate the administration client using the explicit emulator connection string
            _administrationClient = new ServiceBusAdministrationClient(adminConnectionString);
        }

        public async ValueTask DisposeAsync()
        {
            await _client.DisposeAsync();
            await _serviceBus.DisposeAsync();
        }

        [Fact]
        public async Task SendAndReceiveMessage_ShouldReturnMessage()
        {
            const string queueName = "test-queue";
            await CreateQueueIfNotExistsAsync(queueName);

            var sender = _client.CreateSender(queueName);
            var receiver = _client.CreateReceiver(queueName);

            var message = new ServiceBusMessage("Hello Service Bus");
            await sender.SendMessageAsync(message);

            var receivedMessage = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));

            Assert.NotNull(receivedMessage);
            Assert.Equal("Hello Service Bus", receivedMessage.Body.ToString());

            await receiver.CompleteMessageAsync(receivedMessage);
        }

        [Fact]
        public async Task SendMultipleMessages_ShouldReceiveAll()
        {
            const string queueName = "test-queue-multi";
            await CreateQueueIfNotExistsAsync(queueName);
            var sender = _client.CreateSender(queueName);
            var receiver = _client.CreateReceiver(queueName);

            // Send multiple messages
            for (int i = 0; i < 5; i++)
            {
                var message = new ServiceBusMessage($"Message {i}");
                await sender.SendMessageAsync(message);
            }

            // Receive all messages
            var messages = new List<string>();
            for (int i = 0; i < 5; i++)
            {
                var receivedMessage = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));
                if (receivedMessage != null)
                {
                    messages.Add(receivedMessage.Body.ToString());
                    await receiver.CompleteMessageAsync(receivedMessage);
                }
            }

            Assert.Equal(5, messages.Count);
        }

        [Fact]
        public async Task PublishToTopic_ShouldReceiveFromSubscription()
        {
            const string topicName = "test-topic";
            const string subscriptionName = "test-subscription";

            await CreateTopicIfNotExistsAsync(topicName);
            await CreateSubscriptionIfNotExistsAsync(topicName, subscriptionName);

            var sender = _client.CreateSender(topicName);
            var receiver = _client.CreateReceiver(topicName, subscriptionName);

            // Publish message
            var message = new ServiceBusMessage("Topic Message");
            await sender.SendMessageAsync(message);

            // Receive from subscription
            var receivedMessage = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));

            Assert.NotNull(receivedMessage);
            Assert.Equal("Topic Message", receivedMessage.Body.ToString());

            await receiver.CompleteMessageAsync(receivedMessage);
        }

        [Fact]
        public async Task PublishMultipleMessages_TopicSubscription_ShouldReceiveAll()
        {
            const string topicName = "test-topic-multi";
            const string subscriptionName = "test-subscription";

            await CreateTopicIfNotExistsAsync(topicName);
            await CreateSubscriptionIfNotExistsAsync(topicName, subscriptionName);

            var sender = _client.CreateSender(topicName);
            var receiver = _client.CreateReceiver(topicName, subscriptionName);

            // Publish multiple messages
            for (int i = 0; i < 3; i++)
            {
                var message = new ServiceBusMessage($"Topic Message {i}");
                await sender.SendMessageAsync(message);
            }

            // Receive all messages
            var messages = new List<string>();
            for (int i = 0; i < 3; i++)
            {
                var receivedMessage = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));
                if (receivedMessage != null)
                {
                    messages.Add(receivedMessage.Body.ToString());
                    await receiver.CompleteMessageAsync(receivedMessage);
                }
            }

            Assert.Equal(3, messages.Count);
        }

        [Fact]
        public async Task PublishToTopic_MultipleSubscriptions_ShouldReceiveAll()
        {
            const string topicName = "test-topic-broadcast";
            const string subscription1 = "subscription-1";
            const string subscription2 = "subscription-2";

            await CreateTopicIfNotExistsAsync(topicName);
            await CreateSubscriptionIfNotExistsAsync(topicName, subscription1);
            await CreateSubscriptionIfNotExistsAsync(topicName, subscription2);

            var sender = _client.CreateSender(topicName);
            var receiver1 = _client.CreateReceiver(topicName, subscription1);
            var receiver2 = _client.CreateReceiver(topicName, subscription2);

            // Publish message
            var message = new ServiceBusMessage("Broadcast Message");
            await sender.SendMessageAsync(message);

            // Both subscriptions should receive the message
            var receivedMessage1 = await receiver1.ReceiveMessageAsync(TimeSpan.FromSeconds(10));
            var receivedMessage2 = await receiver2.ReceiveMessageAsync(TimeSpan.FromSeconds(10));

            Assert.NotNull(receivedMessage1);
            Assert.NotNull(receivedMessage2);
            Assert.Equal("Broadcast Message", receivedMessage1.Body.ToString());
            Assert.Equal("Broadcast Message", receivedMessage2.Body.ToString());

            await receiver1.CompleteMessageAsync(receivedMessage1);
            await receiver2.CompleteMessageAsync(receivedMessage2);
        }

        [Fact]
        public async Task SendToTopicWithProperties_ShouldReceiveProperties()
        {
            const string topicName = "test-topic-properties";
            const string subscriptionName = "test-subscription";

            await CreateTopicIfNotExistsAsync(topicName);
            await CreateSubscriptionIfNotExistsAsync(topicName, subscriptionName);

            var sender = _client.CreateSender(topicName);
            var receiver = _client.CreateReceiver(topicName, subscriptionName);

            var message = new ServiceBusMessage("Message with Properties")
            {
                ApplicationProperties =
                {
                    { "Priority", "High" },
                    { "Department", "Engineering" }
                }
            };
            await sender.SendMessageAsync(message);

            var receivedMessage = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));

            Assert.NotNull(receivedMessage);
            Assert.Equal("High", receivedMessage.ApplicationProperties["Priority"].ToString());
            Assert.Equal("Engineering", receivedMessage.ApplicationProperties["Department"].ToString());

            await receiver.CompleteMessageAsync(receivedMessage);
        }

        private async Task CreateQueueIfNotExistsAsync(string queueName)
        {
            if (!await _administrationClient.QueueExistsAsync(queueName))
            {
                await _administrationClient.CreateQueueAsync(queueName);
            }
        }

        private async Task CreateTopicIfNotExistsAsync(string topicName)
        {
            if (!await _administrationClient.TopicExistsAsync(topicName))
            {
                await _administrationClient.CreateTopicAsync(topicName);
            }
        }

        private async Task CreateSubscriptionIfNotExistsAsync(string topicName, string subscriptionName)
        {
            if (!await _administrationClient.SubscriptionExistsAsync(topicName, subscriptionName))
            {
                await _administrationClient.CreateSubscriptionAsync(topicName, subscriptionName);
            }
        }
    }
}

To run the test, either use the terminal command below or right-click inside the test file in Visual Studio and click Run Tests.

dotnet test

Azure Cosmos DB Example

Before we start coding, let’s get the dependencies out of the way. You’ll need to install the following NuGet packages (use the CLI or the Visual Studio UI — whichever you prefer).

dotnet add package Microsoft.Azure.Cosmos --version 3.60.0 
dotnet add package Testcontainers.CosmosDb --version 4.12.0

If your first test run hangs or times out, it’s probably just Docker trying to download the image. To keep things smooth, just run this command in your terminal before you start.

docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
using Microsoft.Azure.Cosmos;
using Testcontainers.CosmosDb;

namespace MyTestProject
{
    public class AzureCosmosDbTestExample : IAsyncLifetime
    {
        private readonly CosmosDbContainer _cosmosDb;
        private CosmosClient _client = null!;
        private Database _database = null!;
        private Container _container = null!;

        private const string DatabaseName = "testdb";
        private const string ContainerName = "testcontainer";
        private const string PartitionKeyPath = "/id";

        public record TestItem(string id, string name, int value);

        public AzureCosmosDbTestExample()
        {
            _cosmosDb = new CosmosDbBuilder("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview")
                .WithCleanUp(true)
                .Build();
        }

        public async ValueTask InitializeAsync()
        {
            await _cosmosDb.StartAsync();

            var options = new CosmosClientOptions
            {
                //To fix the "Endpoint not reachable" error in Docker
                ConnectionMode = ConnectionMode.Gateway,
                //It forces the SDK to stay on the Testcontainers port
                LimitToEndpoint = true,
                HttpClientFactory = () =>
                {
                    var handler = new HttpClientHandler
                    {
                        ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
                    };
                    return new HttpClient(handler);
                }
            };

            var connectionString = _cosmosDb.GetConnectionString();
            _client = new CosmosClient(connectionString, options);

            // Create database
            _database = await _client.CreateDatabaseIfNotExistsAsync(DatabaseName);

            // Create container
            _container = await _database.CreateContainerIfNotExistsAsync(
                id: ContainerName,
                partitionKeyPath: PartitionKeyPath);
        }

        public async ValueTask DisposeAsync()
        {
            _client.Dispose();
            await _cosmosDb.DisposeAsync();
        }

        [Fact]
        public async Task InsertAndReadItem_ShouldReturnItem()
        {
            var testItem = new TestItem("test-1", "Test Item", 0);

            // Insert
            await _container.CreateItemAsync(testItem, new PartitionKey(testItem.id));

            // Read
            var response = await _container.ReadItemAsync<TestItem>(testItem.id, new PartitionKey(testItem.id));

            Assert.NotNull(response.Resource);
            Assert.Equal("Test Item", response.Resource.name.ToString());
        }

        [Fact]
        public async Task QueryItems_ShouldReturnMatchingItems()
        {
            // Insert test data
            for (int i = 0; i < 5; i++)
            {
                var item = new { id = $"item-{i}", name = $"Item {i}", category = i % 2 == 0 ? "even" : "odd" };
                await _container.CreateItemAsync(item, new PartitionKey(item.id));
            }

            // Query
            var query = _container.GetItemQueryIterator<dynamic>("SELECT * FROM c WHERE c.category = 'even'");
            var results = new List<dynamic>();

            while (query.HasMoreResults)
            {
                var page = await query.ReadNextAsync();
                results.AddRange(page);
            }

            Assert.Equal(3, results.Count);
        }

        [Fact]
        public async Task UpdateItem_ShouldPersistChanges()
        {
            var item = new TestItem("update-test", "Original", 100);

            // Create
            await _container.CreateItemAsync(item, new PartitionKey(item.id));

            // Update
            var updatedItem = item with { name = "Updated", value = 200 };
            await _container.UpsertItemAsync(updatedItem, new PartitionKey(updatedItem.id));

            // Verify
            var response = await _container.ReadItemAsync<TestItem>(item.id, new PartitionKey(item.id));

            Assert.Equal("Updated", response.Resource.name);
            Assert.Equal(200, response.Resource.value);
        }

        [Fact]
        public async Task DeleteItem_ShouldRemoveFromContainer()
        {
            var item = new TestItem("delete-test", "To Delete", 0);

            // Create
            await _container.CreateItemAsync(item, new PartitionKey(item.id));

            // Delete
            await _container.DeleteItemAsync<TestItem>(item.id, new PartitionKey(item.id));

            // Verify deletion
            var exception = await Assert.ThrowsAsync<CosmosException>(
                () => _container.ReadItemAsync<TestItem>(item.id, new PartitionKey(item.id)));

            Assert.Equal(System.Net.HttpStatusCode.NotFound, exception.StatusCode);
        }
    }
}

The best part is that **Testcontainers works perfectly in CI/CD pipelines like Azure DevOps, GitHub Actions. Since your dependencies run in Docker**, you don’t have to manually install anything on your build agents. It’s the easiest way to catch bugs early and deploy with confidence.

The Short Sign-off

I hope this helps you move away from mocks and toward more reliable integration tests. Cheers,

Hasala Darshana Nadun Kumara

LinkedIn


메타데이터
post_id
567597a4bb27
slug
integration-testing-in-net-with-testcontainers-567597a4bb27
url
https://medium.com/@hasaladarshana2/integration-testing-in-net-with-testcontainers-567597a4bb27
canonical_url
https://medium.com/@hasaladarshana2/integration-testing-in-net-with-testcontainers-567597a4bb27
author_url
https://medium.com/@hasaladarshana2
status
ok
fetched_at
2026-06-09 15:37:30