October 23rd, 2025
mind blowncompellingintriguing3 reactions

Integration Testing with Testcontainers

Introduction

Ever had to write tests that need a real database, and found yourself juggling local setups or dummy data? Testcontainers might just be your new best friend. In this article, we’ll explore how Testcontainers can simplify integration testing by spinning up real services in Docker containers on-demand. We’ll start with a quick intro to what Testcontainers is and why it’s useful, then dive into an example focused on Azure Cosmos DB (a NoSQL database). While Testcontainers supports many databases and services (PostgreSQL, MySQL, MongoDB, Redis, RabbitMQ, you name it), we’ll use Cosmos DB as our example.

What is Testcontainers and Why Use It?

Testcontainers is an open-source framework that makes it easy to run throwaway instances of external resources (databases, message brokers, etc.) in Docker containers during your tests (tech.asimio.net). Instead of manually managing a test database or relying on mocks, you let Testcontainers start a real instance for you. This works across many languages and platforms – including first-class support for Java, Go, Node.js, Python, and .NET (blog.jetbrains.com). Essentially, Testcontainers turns traditionally heavy dependencies into lightweight, ephemeral test instances that your code can interact with as if they were real services (because they are real!).

Why is this useful? Let’s consider testing a database integration. You could use an in-memory store or mocks to simulate a database, but mocks have limitations – they are only as accurate as the assumptions you program into them. As one author put it, “Mocking is fast but typically limited by a developer’s understanding of the underlying technology. Incorrect assumptions can lead to unforeseen errors in production” (blog.jetbrains.com). In contrast, testing against a real database instance (even a local one) gives much higher confidence, because you’re exercising the actual database engine and behaviors (blog.jetbrains.com). The downside of using real instances has traditionally been the complexity of setting them up and the slower speed compared to pure in-memory tests (blog.jetbrains.com) This is exactly where Testcontainers shines: it automates the setup/teardown of real services in containers, so you get the realism of integration tests with much less hassle.

Supported databases/services: Testcontainers provides convenient modules for many popular systems. For example, there are pre-built configurations for PostgreSQL, MySQL, MongoDB, Redis, RabbitMQ, and even cloud service emulators like Azurite (Azure Storage) or Cosmos DB. These modules offer an out-of-the-box experience tailored to each dependency (blog.jetbrains.com). In our case, we’ll use the Cosmos DB emulator module to spin up a local Cosmos DB instance inside a Docker container during our tests.

Integration Testing: Mocks vs Emulators vs Real Services

Before jumping into Cosmos DB, it’s worth understanding the typical approaches for integration testing with databases and why using Testcontainers is beneficial:

  • Unit tests with mocks or in-memory DBs: Fast and easy, but as discussed, they can diverge from real-world behavior. For example, an in-memory implementation might not enforce the same constraints or consistency as the real Cosmos DB engine. This could lead to tests passing in CI but failing in production.
  • Using a real cloud database (test environment in Azure): This ensures you’re testing against the real thing, but it has drawbacks. It can be slow (network calls) and costly. “Your integration tests might be connecting to a dedicated Cosmos DB hosted in Azure, which increases your organization’s subscription cost.” (tech.asimio.net)

    Also, running tests against a cloud resource can be flaky (network issues) and is not always feasible for every run (e.g., running on a local dev machine or in CI without cloud credentials).

  • Using a local Cosmos DB emulator manually: Azure provides a Cosmos DB Emulator that you can run locally (as a Windows app or Docker container) to mimic Cosmos DB’s behavior without cloud costs. This is great for development – “Using the emulator for development can help you learn… without incurring any service costs” (learn.microsoft.com). However, if you run it manually, you need to ensure it’s installed, started, and listening on the correct ports before running tests. For instance, if using the Docker emulator, you’d have to pull the image and run docker run with the proper ports and environment variables each time. One blog notes “you would need to make sure [the emulator] is running, and which ports it listens on, before you seed test data and run every test.” (tech.asimio.net).

    This manual setup can be error-prone and doesn’t scale well to automated test pipelines.

  • Using Testcontainers (local emulator automated): This approach combines the benefits of the emulator with automation. TestContainers will pull and start the Cosmos DB Emulator Docker image for you as part of the test setup, wait until it’s ready, and then provide your test code the connection details (like the URI and key to connect). When tests finish, it can shut down and clean up the container. This means each test (or test suite) gets a fresh Cosmos DB instance that behaves like the real service, without any manual steps. You don’t have to maintain a live emulator running in the background or an Azure test account, and you can run the same integration tests on any machine (including in CI) reliably. In short: realistic environment, automated setup/teardown, and no cloud costs. 🎉

Now, let’s walk through how to do this in a .NET environment with the Azure Cosmos DB Emulator and TestContainers. We’ll use C# and assume you have the Testcontainers for .NET library installed (specifically, we’ll use the Testcontainers.CosmosDb NuGet module for Cosmos DB).

Setting Up a Cosmos DB Emulator Container in a Test

The first step is to configure and launch the Cosmos DB Emulator container for our test. TestContainers for .NET provides a CosmosDbBuilder (in the DotNet.Testcontainers.Builders namespace) which makes this easy. We’ll use it to specify the Docker image and any required settings, then start the container. Here’s how we can do it:

using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
// ... in your test class setup method:

var cosmosDbContainer = new CosmosDbBuilder()
    .WithImage("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest")
    .WithEnvironment("AZURE_COSMOS_EMULATOR_IP_ADDRESS_OVERRIDE", "127.0.0.1") // ensure emulator uses localhost
    .WithPortBinding(8081, 8081) // map container's default port to host port 8081
    .Build();

// Start the Cosmos DB Emulator container (this may take a few seconds to initialize)
await cosmosDbContainer.StartAsync();

Let’s break down what’s happening here:

  • Docker image: We’re using the official Azure Cosmos DB Linux emulator image: "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest". This image runs a local Cosmos DB instance inside a container. Testcontainers will pull this image if it’s not already available.
  • Environment variable – AZURE_COSMOS_EMULATOR_IP_ADDRESS_OVERRIDE: This is a crucial setting for the Cosmos DB emulator in Docker. It tells the emulator to treat a specific IP (here 127.0.0.1 for localhost) as the host. Without this, the emulator might advertise its internal container IP, which our host machine (or test process) can’t reach (See: testcontainers-cosmosdb-always-gets-stuck-whenever-making-any-cosmosclient-api-c#). Setting this env var ensures that the emulator will use localhost for URIs, so that our test can connect to https://localhost:8081. In short, it avoids a situation where the SDK tries to connect to an unreachable IP address.
  • Port binding: The Cosmos emulator by default uses port 8081 for its SQL (Core API) endpoint (and some additional ports 10250-10255 for other purposes). In the code above, we bind the container’s port 8081 to the host’s port 8081. This means after starting, we can reach the emulator at https://127.0.0.1:8081 from our test. You can also let Testcontainers assign a random host port (to avoid conflicts) by using .WithPortBinding(8081, true) which would dynamically map 8081 to a free port (blog.jetbrains.com). For simplicity, we used the same port here, assuming no other service is using 8081 on our machine.
  • Starting the container: StartAsync() will launch the Docker container and block (async) until the emulator is fully up and running. The Cosmos DB emulator can take a few seconds to initialize (it has to start the service inside the container), but Testcontainers will handle the wait. Once this call returns, we have a running Cosmos DB we can connect to.

At this point, the emulator is running. If you were to navigate your browser to https://localhost:8081/_explorer/index.html you would even see the Cosmos DB Emulator’s data explorer UI (just as a sanity check, though it’s not needed for the test to work).

Initializing the CosmosClient and Preparing the Database

Now that we have a running Cosmos DB instance in a container, the next step is to connect to it from our test code. Azure Cosmos DB is accessed via the Cosmos SDK (which in .NET provides the CosmosClient class). Usually, you’d need the endpoint URI and an authorization key to create a CosmosClient. The Testcontainers library helps here by providing these connection details from the running container.

In our example, cosmosDbContainer (the object we started) can give us a connection string or the individual parts needed. For simplicity, we’ll use the connection string property, which includes both the URI and the key. Then we’ll use that to create a CosmosClient instance. We also need to configure the client to ignore SSL certificate validation for the emulator, because the emulator uses a self-signed certificate. (In a production scenario you would not disable SSL validation, but here it’s okay in the test environment.)

Let’s set up the client and a fresh database + container for our test data:

// Build CosmosClient with the emulator's connection string
var connectionString = cosmosDbContainer.GetConnectionString();
// e.g. "AccountEndpoint=https://127.0.0.1:8081/;AccountKey=C2y6y...==;"

// Configure client options to ignore the emulator's self-signed SSL cert
var clientOptions = new CosmosClientOptions
{
    ConnectionMode = ConnectionMode.Gateway, // use HTTP mode
    HttpClientFactory = () => new HttpClient(new HttpClientHandler {
        ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
    })
};

using var cosmosClient = new CosmosClient(connectionString, clientOptions);

// Create a test database and container inside the emulator
string databaseName = "TestDatabase";
string containerName = "TestContainer";
await cosmosClient.CreateDatabaseIfNotExistsAsync(databaseName);
var database = cosmosClient.GetDatabase(databaseName);

// Define a partition key path (e.g., use "/id" as the partition key for simplicity)
await database.CreateContainerIfNotExistsAsync(containerName, "/id");
var container = database.GetContainer(containerName);

Here’s what we’re doing in this code:

  • We retrieve the connection string from the container. The Testcontainers CosmosDbContainer (the object returned by CosmosDbBuilder().Build()) typically provides a method like GetConnectionString() or properties for Hostname, Port, and Key. The connection string is convenient as it packages the endpoint and key in the format the Cosmos SDK expects (as if it were the string you’d get from the Azure portal or emulator). It will look something like: AccountEndpoint=https://127.0.0.1:8081/;AccountKey=YOUR_EMULATOR_KEY_HERE;
  • We configure CosmosClientOptions to bypass SSL certificate validation. The emulator’s SSL certificate isn’t trusted by our machine by default (since it’s self-signed), so without this, the SDK would throw an exception about an untrusted cert. We use the DangerousAcceptAnyServerCertificateValidator callback only in the test context to skip verification. Another option is to import the emulator’s certificate into the trusted store, but doing it programmatically is a bit involved – ignoring SSL is simpler for our test. We also set ConnectionMode = Gateway to ensure the client uses the HTTPS endpoint (as opposed to trying direct TCP connections, which the emulator might not support).
  • We create a CosmosClient using the connection string and our options. Now we have a client connected to the local emulator.
  • Next, we create a fresh database and container for the test. Even though the emulator is running, it’s empty initially – no databases, containers, or data. This is good because each test can set up exactly what it needs from scratch. We call CreateDatabaseIfNotExistsAsync("TestDatabase") to create a new database (or get it if somehow it already existed from a previous run). Then on that database, we call CreateContainerIfNotExistsAsync("TestContainer", "/id") to create a container with partition key path "/id". In Cosmos DB, every item needs a partition key; here we’re simply using the id property of documents as the partition key for simplicity. Finally, we obtain a reference to that container via GetContainer.

At this point, our integration test environment is ready: we have a running Cosmos DB emulator and an empty container to store items. Now let’s actually use it in a test case.

Test: Inserting and Querying a Document in Cosmos DB

For our example test, we’ll simulate a simple scenario: insert a document into the Cosmos DB container and then read it back to verify that the data was stored and retrieved correctly. This will prove that our Cosmos DB integration (via the emulator) is working within the test.

Imagine we have a simple data model, say a User or Item with an id and a Name. We don’t even need to formally define a class for this small test – we can use an anonymous object or a dynamic type. But to keep things clear, I’ll define a small record class for our test item:

public record UserItem(string id, string Name);

Now, here’s the test logic:

// Arrange: create a test item
var testUser = new UserItem(Guid.NewGuid().ToString(), "Jane Doe");

// Act: insert the item into the Cosmos container
await container.CreateItemAsync(testUser, new PartitionKey(testUser.id));

// Act: try to read the item back from the container
var response = await container.ReadItemAsync<UserItem>(
    testUser.id,
    new PartitionKey(testUser.id)
);
UserItem fetchedUser = response.Resource;

// Assert: the fetched item matches what we inserted
Assert.Equal(testUser.Name, fetchedUser.Name);

Let’s break that down:

  • We create a new UserItem with a unique id (using Guid.NewGuid().ToString() to generate a random ID) and a name “Jane Doe”. This object will be our test document to insert.
  • We call CreateItemAsync on the Cosmos container to insert the item. We provide the item and the partition key value. Since we decided the partition key path is “/id”, we use testUser.id as the partition key. This call will write the item to the emulator’s database. If everything is configured correctly, it should succeed and the item will be stored.
  • Next, we attempt to read the item back by ID. We use ReadItemAsync<UserItem> to fetch the document and deserialize it into a UserItem instance. We again provide the partition key (which is the id). Cosmos DB requires the partition key for point reads so it knows where to look for the item.
  • The result comes back in a response object which has a Resource property containing the fetched UserItem. We extract that as fetchedUser.
  • Finally, we assert that the Name of the fetched user equals the Name of the original user we inserted. In our example, that should be “Jane Doe”. If the data round-trip succeeded, the assertion will pass 🎉. If something was misconfigured (for example, if the container wasn’t actually created or the item wasn’t written), this might throw or return a null resource, causing the assert to fail. In a real test, you’d have more robust checks and error handling, but this suffices to illustrate the concept.

At this point, we have a passing integration test that actually communicated with a real Cosmos DB (emulator) instance inside a Docker container. We didn’t have to set up any Azure resources or maintain a running database manually; Testcontainers did that heavy lifting for us.

Cleanup: Tearing Down the Container

One of the advantages of Testcontainers is that it handles cleanup for you if you use it properly. When the test finishes, we should ensure to dispose of the CosmosClient and stop the container. In many testing frameworks (like xUnit), you can use fixtures or the IAsyncLifetime interface to manage setup and teardown. For illustration, we’ll show the cleanup steps explicitly:

// (Cleanup logic after tests run)
await cosmosClient.GetDatabase(databaseName).DeleteAsync(); // delete the database (optional)
await cosmosDbContainer.DisposeAsync();  // stop and remove the container

A few notes on cleanup:

  • We deleted the database we created (TestDatabase). This step is actually optional when using an emulator container; since the whole Cosmos DB instance is running in a throwaway container, when we dispose of the container, everything inside it goes away anyway. However, I included the explicit database deletion to illustrate how you’d clean up data if you were reusing the emulator across multiple tests (you might want to start fresh each time).
  • Calling DisposeAsync() on the container will gracefully shut down and remove the Docker container. In our example, we started the container at the beginning of the test; disposing it at the end ensures we don’t leave a stray emulator running. This is important especially in CI environments or when running many tests, to avoid consuming resources unnecessarily. If you instead use a using var cosmosDbContainer = new CosmosDbBuilder()...Build(); pattern, disposing could be automatic.
  • If using xUnit, you could put the container startup in InitializeAsync and disposal in DisposeAsync (via IAsyncLifetime or fixture), so that each test class gets its own isolated database environment. This way, tests won’t interfere with each other.

At this point, our test has fully cleaned up after itself. The Cosmos DB emulator container is gone, and with it all the data we inserted. Each test run starts with a blank slate.

Complete Example: Cosmos DB Integration Tests Using Testcontainers

In this example, you’ll see how to use Testcontainers with the Azure Cosmos DB emulator in a full MSTest class. The sample demonstrates:

  • Conditional Test Execution: It checks an environment variable so that integration tests only run when desired.
  • Container Setup: Using the ContainerBuilder to configure the emulator image, set required environment variables, and define port bindings.
  • Client Initialization: Creating a CosmosClient with a custom HTTP handler to bypass SSL validation (needed for the emulator).
  • Test Execution: Inserting and querying a document, with assertions using FluentAssertions.
  • Cleanup: Properly stopping and disposing of the container and deleting the test database to ensure a clean state between runs.

The complete sample code is provided below—use it as-is in your test project:

// <copyright file="CosmosDbIntegrationTests.cs" company="Microsoft">
// Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.
// </copyright>

namespace Microsoft.EvalSdk.Tests.Integration
{
    using System;
    using System.Net;
    using System.Security.Authentication;
    using System.Threading.Tasks;
    using FluentAssertions;
    using global::DotNet.Testcontainers.Builders;
    using global::DotNet.Testcontainers.Containers;
    using Microsoft.Azure.Cosmos;
    using Microsoft.EvalSdk.Tests.TestUtils;

    [TestClass]
    [TestCategory("Integration")]
    public class CosmosDbIntegrationTests
    {
        private const string PrimaryKey = "C2y6yDjf5/R+ob0N8A7Cgv30VR=="; // Emulator default key
        private const string DatabaseId = "TestDatabase";
        private const string ContainerId = "TestContainer";
        private static IContainer? cosmosEmulatorContainer; // The Testcontainer instance for the Cosmos DB Emulator.
        private CosmosClient? cosmosClient;
        private Database? database;
        private Container? container;

        [ClassInitialize]
        public static async Task ClassInitialize(TestContext context)
        {
            // Check if integration tests should run.
            if (Environment.GetEnvironmentVariable("RUN_INTEGRATION_TESTS") != "true")
            {
                Assert.Inconclusive("Integration tests are skipped. Set RUN_INTEGRATION_TESTS=true to run them.");
            }

            Environment.SetEnvironmentVariable("TESTCONTAINERS_RYUK_DISABLED", "true");

            cosmosEmulatorContainer = new ContainerBuilder()
                .WithImage("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview")
                .WithPortBinding(8081, 8081)
                .WithPortBinding(1234, 1234)
                .WithEnvironment("AZURE_COSMOS_EMULATOR_ALLOW_INSECURE_CONNECTIONS", "true")
                .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(8081))
                .WithCreateParameterModifier(parameters =>
                {
                    parameters.Tty = false; // Disable TTY allocation
                })
                .WithOutputConsumer(NullOutputConsumer.Instance)
                .Build();

            await cosmosEmulatorContainer.StartAsync();
        }

        [ClassCleanup]
        public static async Task ClassCleanup()
        {
            if (cosmosEmulatorContainer != null)
            {
                try
                {
                    await cosmosEmulatorContainer.StopAsync();
                }
                catch
                {
                    // Ignore exceptions during cleanup
                }

                await cosmosEmulatorContainer.DisposeAsync();
            }
        }

        [TestInitialize]
        public async Task TestInitialize()
        {
            var mappedPort = cosmosEmulatorContainer!.GetMappedPublicPort(8081);
            var endpointUrl = $"http://{cosmosEmulatorContainer.Hostname}:{mappedPort}";

            // Configure HTTP client handler to bypass certificate validation.
            var httpClientHandler = new HttpClientHandler
            {
                ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
                SslProtocols = SslProtocols.Tls12,
            };

            var options = new CosmosClientOptions
            {
                ConnectionMode = ConnectionMode.Gateway,
                HttpClientFactory = () => new HttpClient(httpClientHandler),
            };

            this.cosmosClient = new CosmosClient(endpointUrl, PrimaryKey, options);
            this.database = await this.cosmosClient.CreateDatabaseIfNotExistsAsync(DatabaseId);
            this.container = await this.database.CreateContainerIfNotExistsAsync(ContainerId, "/id");
        }

        [TestCleanup]
        public async Task TestCleanup()
        {
            if (this.database != null)
            {
                await this.database.DeleteAsync();
            }

            this.cosmosClient?.Dispose();
        }

        [TestMethod]
        public async Task CreateAndQueryDocument_ShouldSucceed()
        {
            // Arrange
            var testItem = new { id = Guid.NewGuid().ToString(), Name = "Test", Value = 123 };

            // Act
            var createResponse = await this.container!.CreateItemAsync<object>(testItem, new PartitionKey(testItem.id));
            createResponse.StatusCode.Should().Be(HttpStatusCode.Created);

            string sqlQueryText = "SELECT * FROM c WHERE c.id = @id";
            var queryDefinition = new QueryDefinition(sqlQueryText)
                .WithParameter("@id", testItem.id);
            var queryIterator = this.container.GetItemQueryIterator<dynamic>(queryDefinition);
            var queryResult = await queryIterator.ReadNextAsync();

            // Assert
            queryResult.Count.Should().BeGreaterThan(0);
            dynamic document = queryResult.First();
            ((string)document.id).Should().Be(testItem.id);
        }
    }
}

How to Use This Example

  • Integration Test Activation: The test class first checks if the environment variable RUN_INTEGRATION_TESTS is set to “true“. This means that in your local environment or CI pipeline, you can control whether these integration tests run or not.
  • Container Initialization: The ClassInitialize method sets up the Cosmos DB emulator container using Testcontainers. It binds the necessary ports, sets environment variables (like allowing insecure connections), and defines a wait strategy to ensure the emulator is fully operational before tests run.
  • Test Lifecycle Management: The [TestInitialize] and [TestCleanup] methods ensure that for each test run, a fresh Cosmos DB instance is available and then properly cleaned up. This isolation helps maintain test reliability and prevents interference between tests.
  • Running the Test: The CreateAndQueryDocument_ShouldSucceed method demonstrates a full round-trip: inserting a document and querying it back. Using FluentAssertions, it verifies that the insertion and retrieval work as expected, confirming that your integration test setup with the emulator is valid.

By using this complete example, you get a self-contained integration test that leverages Testcontainers to automate the setup and teardown of the Cosmos DB emulator—ensuring your tests run in an environment that closely resembles production without the overhead of managing external resources manually.

Recap and Next Steps

In this article, we demonstrated how to use Testcontainers in .NET to integration-test against a real Azure Cosmos DB environment, without requiring any external infrastructure. We covered:

  • What Testcontainers is – a library to run ephemeral Docker containers for testing, supporting many databases and services (from SQL databases to NoSQL stores and more) (blog.jetbrains.com).
  • Why use it – to get higher confidence in tests by using real services, but with the convenience of automation. We compared it to alternative approaches like using mocks (fast but less realistic (blog.jetbrains.com)) or maintaining cloud/local test environments (real but hard to manage and potentially costly (tech.asimio.net)).
  • Setting up Cosmos DB for tests – using the Cosmos DB Docker emulator image with Testcontainers, configuring environment variables and ports so our test can talk to it.
  • Writing a test that inserts and reads data from Cosmos DB via the CosmosClient as a proof of concept that our setup works.
  • Cleaning up after the test by disposing the container (ensuring no leftover processes or data).

Where to go next? If you’re interested in using Testcontainers for your own projects, here are a few pointers:

  • Check out the official Testcontainers documentation for .NET (and other languages) to see all the available modules and features. The .NET docs on GitHub and the Testcontainers website list modules for various technologies (SQL databases, message queues, etc.) and show example usage for each.
  • Read more on the Azure Cosmos DB Emulator in Microsoft’s docs, especially if you need to tweak its behavior. The emulator supports flags for enabling the MongoDB API, data persistence, etc., which you can set via environment variables (as we did for the IP override). The docs on “Develop locally using the Azure Cosmos DB emulator” (learn.microsoft.com) are a great resource to understand what the emulator can do and how to use it in CI pipelines.
  • Try extending this example: for instance, integration test a repository or data access layer from your application that uses Cosmos DB. Instead of directly using CosmosClient in the test, you could instantiate your repository class, point it at the emulator (using the connection string), and verify that higher-level operations work correctly. Testcontainers will make sure Cosmos DB is there for your repository to use.
  • Explore other databases with Testcontainers. The pattern is very similar: for example, to test against PostgreSQL you might use new PostgreSqlBuilder()...Build() (after adding the appropriate NuGet package) and then get a connection string to initialize an Npgsql DbConnection. Using Testcontainers, you can run integration tests with MySQL, MongoDB, Redis, Kafka, RabbitMQ, and many others with just a few lines of setup.

By integrating Testcontainers into your test suite, you can move beyond mocks and ensure your code works against real infrastructure components, all while keeping tests automated and self-contained. Happy testing!

Resources

The feature image was generated using Bing Image Creator. Terms can be found in the Bing Image Creator Terms of Use.

Category
CSE
Topics
ISE

Author