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.
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 categoryGET /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)
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 X, YouTube, and LinkedIn.
To easily build your first database, watch our Get Started videos on YouTube and explore ways to dev/test free.
0 comments
Be the first to start the discussion.