Integration Testing Go with Docker: From Theory to Advanced Practice
\n\nIntegration testing is a critically important stage in developing microservices in Go. Without verifying interactions with databases, message queues, and external APIs, you risk getting "it works on my machine" in its most unpleasant form. Docker allows you to spin up clean, isolated environments for each test, and advanced techniques allow you to do it quickly, in parallel, and without pain. In this article, we will break down the real approaches used by senior engineers.
\n\n1. Why Docker is the Ideal Partner for Integration Tests in Go
\n\nGo compiles into a static binary, making it an ideal candidate for containerization. In integration testing, Docker solves three key tasks:
\n- \n
- Isolation: each test gets a fresh database or Redis without "garbage" from previous runs. \n
- Reproducibility: tests work identically on a developer's machine, in CI/CD, and in production. \n
- Speed: properly configured containers start in seconds, not minutes. \n
The main mistake beginners make is running one container for all tests. This leads to race conditions and "flaky" bugs. The advanced approach: one container per test package or even per individual test.
\n\n2. Testcontainers for Go: Managed Containers from Code
\n\nThe testcontainers-go library allows you to manage Docker containers directly from Go tests. This is more elegant than writing bash scripts or relying on a pre-started Docker Compose.
Installation:
\ngo get github.com/testcontainers/testcontainers-go\ngo get github.com/testcontainers/testcontainers-go/modules/postgres\n\nExample: Testing PostgreSQL with migrations:
\npackage integration_test\n\nimport (\n "context"\n "database/sql"\n "testing"\n "time"\n\n "github.com/testcontainers/testcontainers-go"\n "github.com/testcontainers/testcontainers-go/modules/postgres"\n "github.com/testcontainers/testcontainers-go/wait"\n _ "github.com/lib/pq"\n)\n\nfunc TestUserRepository(t *testing.T) {\n ctx := context.Background()\n\n pgContainer, err := postgres.RunContainer(ctx,\n testcontainers.WithImage("postgres:16-alpine"),\n postgres.WithDatabase("testdb"),\n postgres.WithUsername("user"),\n postgres.WithPassword("password"),\n testcontainers.WithWaitStrategy(\n wait.ForLog("database system is ready to accept connections").\n WithOccurrence(2).\n WithStartupTimeout(30*time.Second),\n ),\n )\n if err != nil {\n t.Fatal(err)\n }\n defer pgContainer.Terminate(ctx)\n\n connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")\n if err != nil {\n t.Fatal(err)\n }\n\n db, err := sql.Open("postgres", connStr)\n if err != nil {\n t.Fatal(err)\n }\n defer db.Close()\n\n // Run migrations and tests here\n _, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL\n )`)\n if err != nil {\n t.Fatal(err)\n }\n\n // Test the repository\n repo := NewUserRepository(db)\n user, err := repo.Create(ctx, "Alice")\n if err != nil {\n t.Fatal(err)\n }\n if user.Name != "Alice" {\n t.Errorf("expected Alice, got %s", user.Name)\n }\n}\n\nKey points:
\n- \n
WithWaitStrategy— wait until PostgreSQL is actually ready, not just the container is started. \n defer pgContainer.Terminate(ctx)— guarantees cleanup even on panic. \n - Each test gets its own database instance — complete isolation. \n
3. Parallel Tests and Resource Management
\n\nGo supports parallel