July 21st, 2025
0 reactions

Integration testing for Go applications using Testcontainers and containerized databases

Abhishek Gupta
Principal Product Manager

Integration testing has always presented a fundamental challenge: how do you test your application against real dependencies without the complexity of managing external services? Traditional approaches often involve either mocking dependencies (which can miss integration issues) or maintaining separate test environments (which can be expensive and difficult to manage consistently).

Hello Testcontainers!

Testcontainers solves this problem elegantly by providing a way to run lightweight, throwaway instances of databases, message brokers, web servers, and other services directly within your test suite. Instead of complex setup scripts or shared test environments, you can spin up real services in Docker containers that exist only for the duration of your tests. The core value proposition is compelling: write tests that run against the actual technologies your application uses in production, while maintaining the isolation and repeatability that good tests require. When your tests complete, the containers are automatically cleaned up, leaving no trace behind.

Testcontainers for Go (testcontainers-go package) brings this powerful testing approach to the Go ecosystem. It provides a clean, idiomatic Go API for creating and managing Docker containers within your test suite, handling the lifecycle management, port mapping, and health checks automatically.

This post walks through a practical example of using testcontainers-go with the Azure Cosmos DB emulator. Azure Cosmos DB offers fully-featured Docker containers that mimic the behavior of the cloud service, making it an excellent candidate for integration testing with Testcontainers. Whether you’re building applications that will eventually run against Azure Cosmos DB, or you’re simply exploring document database patterns, the emulator provides a realistic testing environment without requiring cloud resources.

There are two flavors of the Azure Cosmos DB emulator. In this example, I have used the Linux-based emulator (in preview) (which runs, almost anywhere, including ARM64 architecture). You are welcome to try out the other version as well.

We’ll examine a simple application with a focus on how testcontainers handles the container setup, test execution, and cleanup. The app uses the Go SDK for Azure Cosmos DB (azcosmos package) and exposes a simple REST API that manages items in a container:

  • POST /items – Creates a new item with name, description, and category
  • GET /items/{id}?category={category} – Retrieves an item by ID and category

Testcontainers and Azure Cosmos DB emulator in action

Let’s see testcontainers in action with a practical example. The tests demonstrate how testcontainers automatically spins up a container (for the emulator), runs our integration tests against it, and cleans up afterward. This gives us the confidence of testing against real database behavior without the complexity of managing external services.

Running the tests

Before we examine the code, let’s run the tests to see testcontainers in action. Make sure you have Go installed and Docker running on your machine. If you don’t have Docker installed, you can follow the instructions on the Docker website.

Pull the Azure Cosmos DB emulator image from Docker Hub:

docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview

Next, clone the repository and run the tests. The test suite will automatically start the emulator in a Docker container, run the integration tests, and then clean up the container afterward.

git clone github.com/abhirockzz/cosmosdb-testcontainers-go
cd cosmosdb-testcontainers-go

go test -v ./...

You should see the tests passing with output similar to this (some lines removed for brevity):

github.com/testcontainers/testcontainers-go - Connected to docker: 
  Server Version: 28.3.0
  API Version: 1.48
  Operating System: Docker Desktop
  Total Memory: 7836 MB
//.....
🐳 Creating container for image mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
🐳 Creating container for image testcontainers/ryuk:0.11.0
✅ Container created: 5e43ec4bd8ea
🐳 Starting container: 5e43ec4bd8ea
✅ Container started: 5e43ec4bd8ea
⏳ Waiting for container id 5e43ec4bd8ea image: testcontainers/ryuk:0.11.0. Waiting for: &{Port:8080/tcp timeout:<nil> PollInterval:100ms skipInternalCheck:false}
🔔 Container is ready: 5e43ec4bd8ea
✅ Container created: cb1d2abb7255
🐳 Starting container: cb1d2abb7255
✅ Container started: cb1d2abb7255
⏳ Waiting for container id cb1d2abb7255 image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview. Waiting for: &{Port:8081 timeout:<nil> PollInterval:100ms skipInternalCheck:false}
🔔 Container is ready: cb1d2abb7255
=== RUN   TestCreateItem
--- PASS: TestCreateItem (0.00s)
=== RUN   TestCreateItem_FailureScenarios
//.....
--- PASS: TestCreateItem_FailureScenarios (0.00s)
    --- PASS: TestCreateItem_FailureScenarios/missing_name (0.00s)
    --- PASS: TestCreateItem_FailureScenarios/missing_category (0.00s)
    --- PASS: TestCreateItem_FailureScenarios/invalid_JSON (0.00s)
    --- PASS: TestCreateItem_FailureScenarios/wrong_HTTP_method (0.00s)
=== RUN   TestGetItem
--- PASS: TestGetItem (0.00s)
=== RUN   TestGetItem_FailureScenarios
//....
--- PASS: TestGetItem_FailureScenarios (0.00s)
    --- PASS: TestGetItem_FailureScenarios/missing_item_ID (0.00s)
    --- PASS: TestGetItem_FailureScenarios/missing_category_parameter (0.00s)
    --- PASS: TestGetItem_FailureScenarios/empty_category_parameter (0.00s)
    --- PASS: TestGetItem_FailureScenarios/non-existent_item_ID (0.00s)
    --- PASS: TestGetItem_FailureScenarios/valid_item_ID_but_wrong_category (0.00s)
PASS
🐳 Stopping container: cb1d2abb7255
✅ Container stopped: cb1d2abb7255
🐳 Terminating container: cb1d2abb7255
🚫 Container terminated: cb1d2abb7255
ok      demo    9.194s

Understanding the test flow

The output shows how testcontainers orchestrates the entire process in three distinct phases:

  • Container Setup Phase: testcontainers connects to Docker, starts the emulator and Ryuk containers, and waits until the emulator is ready for use.
  • Test Execution Phase: Integration tests run against the live container, covering both successful operations and various error scenarios to ensure robust handling.
  • Cleanup Phase: All containers are automatically stopped and removed, ensuring the system is left clean with no lingering resources.

The combination of testcontainers and the emulator gives you the reliability of testing against services without the operational overhead of managing test infrastructure. Every test run is isolated, repeatable, and starts from a known clean state. It gives you fast feedback while maintaining the confidence that comes from testing against actual database behavior.

Deep dive

Now let’s examine how this works by walking through the code and exploring the key components that make this work.

Orchestrating the test environment

The TestMain function serves as the entry point for our test suite, providing a way to perform setup and teardown operations that span multiple test functions. Unlike individual test functions that run in isolation, TestMain runs once per package and gives you control over the entire test execution lifecycle.

func TestMain(m *testing.M) {
    // Set up the CosmosDB emulator container
    ctx := context.Background()

    var err error
    emulator, err = setupCosmosDBEmulator(ctx)
    if err != nil {
        fmt.Printf("Failed to set up CosmosDB emulator: %v\n", err)
        os.Exit(1)
    }

    // ... client setup and data seeding

    // Run the tests
    code := m.Run()

    // Cleanup
    if emulator != nil {
        _ = emulator.Terminate(ctx)
    }

    os.Exit(code)
}

This structure ensures that the expensive operation of starting a Docker container happens only once, while all individual tests can run against the same container instance. The pattern is particularly valuable when your setup involves external resources like databases or message brokers.

Setting up the Azure Cosmos DB emulator

The setupCosmosDBEmulator function encapsulates the logic for creating and starting the emulator:

func setupCosmosDBEmulator(ctx context.Context) (testcontainers.Container, error) {

    req := testcontainers.ContainerRequest{
        Image:        emulatorImage,
        ExposedPorts: []string{emulatorPort + ":8081"},
        WaitingFor:   wait.ForListeningPort(nat.Port(emulatorPort)),
        Env: map[string]string{
            "ENABLE_EXPLORER": "false",
            "PROTOCOL":        "https",
        },
    }

    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })

    // Give the emulator a bit more time to fully initialize
    time.Sleep(5 * time.Second)

    return container, nil
}

The ContainerRequest struct defines everything testcontainers needs to know about our desired container. The ExposedPorts field maps the container’s internal port 8081 to the same port on the host, while WaitingFor ensures testcontainers doesn’t consider the container ready until the emulator is actually listening on that port. The environment variables configure the emulator to run in HTTPS (PROTOCOL) mode without the web-based data explorer (ENABLE_EXPLORER is set to false). I encourage you to explore the documentation for more configuration options that might suit your testing needs.

Connecting to the emulator

The auth.GetEmulatorClientWithAzureADAuth function is part of the cosmosdb-go-sdk-helper package. It provides a convenient way to create a Azure Cosmos DB client that uses Microsoft Entra ID authentication, which is the recommended approach for production applications. However, in this test setup, GetEmulatorClientWithAzureADAuth creates a client instance configured specifically for the emulator environment. By passing the custom transport through the ClientOptions, we ensure that the SDK can successfully connect to the emulator despite its self-signed certificate.

transport := &http.Client{Transport: &http.Transport{
    TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}}

options := &azcosmos.ClientOptions{ClientOptions: azcore.ClientOptions{
    Transport: transport,
}}

client, err = auth.GetEmulatorClientWithAzureADAuth(emulatorEndpoint, options)

Note that InsecureSkipVerify: true should not be used in production environments.

Test execution and cleanup

After the container is ready and the client is configured, the code seeds test data through seedTestData() to provide known items for the tests to work with. m.Run() then executes all the actual test functions, returning an exit code that indicates whether the tests passed or failed.

Finally, the emulator.Terminate(ctx) call in the cleanup section ensures that testcontainers properly stops and removes the Docker container, regardless of the test results. This cleanup is essential for preventing resource leaks and ensuring that subsequent test runs start with a clean slate.

Conclusion

With the right tools, the complexity of integration testing can be tamed. Testcontainers bridges this gap by making it easier to test against containerised services while maintaining isolation, repeatability, and eliminating the need for shared infrastructure.

While this example focused on Azure Cosmos DB, the patterns apply broadly to any service that offers Docker containers, including PostgreSQL, Kafka, and more. Start with a single service and expand your test coverage gradually.

Although the Azure Cosmos DB emulator is a powerful tool for local development and testing, but it does not support every feature available in the full Azure Cosmos DB service. You can can read on the Differences between the emulator and cloud service, or consult the documentation to understand the subset of features that are supported in the emulator.

The full example code is available in the GitHub repository, and the testcontainers-go documentation provides comprehensive guidance for exploring more.

Happy building!

About Azure Cosmos DB

Azure Cosmos DB is a fully managed and serverless distributed database for modern app development, with SLA-backed speed and availability, automatic and instant scalability, and support for open-source PostgreSQL, MongoDB, and Apache Cassandra. To stay in the loop on Azure Cosmos DB updates, follow us on XYouTube, and LinkedIn.

To easily build your first database, watch our Get Started videos on YouTube and explore ways to dev/test free.

Author

Abhishek Gupta
Principal Product Manager

Principal Product Manager in the Azure Cosmos DB team.

0 comments