Integration Testing Go with Docker: Complete Guide
Integration testing is a key stage in developing reliable Go applications. Unlike unit tests, integration tests verify component interactions: databases, caches, external APIs. Docker allows isolating the test environment, making tests reproducible and independent of the host machine. In this article, we'll explore how to set up integration testing for Go applications with Docker — from simple containers to advanced scenarios with Testcontainers and CI/CD.
1. Why Docker for Go Integration Testing?
Go applications often work with PostgreSQL, Redis, MongoDB, or other services. Manually configuring these dependencies on every developer machine and CI server is a path to errors. Docker solves this problem:
- Reproducibility — identical environment locally and in CI.
- Isolation — tests don't clutter the host system.
- Speed — containers start in seconds.
- Parallelism — you can run multiple isolated tests simultaneously.
Let's look at a basic example: testing a function that saves data to PostgreSQL.
2. Setting Up Test Environment with Docker Compose
Create a docker-compose.test.yml for the test database:
version: '3.8'services: postgres_test: image: postgres:16-alpine environment: POSTGRES_USER: test POSTGRES_PASSWORD: test POSTGRES_DB: testdb ports: - "5433:5432" # use non-standard port to avoid interfering with local DB tmpfs: /var/lib/postgresql/data # data in memory for speedIn the Go test, start the container via os/exec or use the testcontainers-go library. We'll show both approaches.
2.1. Manual Docker Compose Launch from Test
// main_test.gopackage main
import ( "context" "fmt" "os/exec" "testing" "time")
func setupTestDB(t *testing.T) string { t.Helper() // Start the container cmd := exec.Command("docker-compose", "-f", "docker-compose.test.yml", "up", "-d") if err := cmd.Run(); err != nil { t.Fatalf("failed to start docker-compose: %v", err) } // Wait for PostgreSQL readiness time.Sleep(3 * time.Second) return "postgres://test:test@localhost:5433/testdb?sslmode=disable"}
func TestSaveUser(t *testing.T) { dsn := setupTestDB(t) db, err := sql.Open("postgres", dsn) if err != nil { t.Fatal(err) } defer db.Close() // ... test code}Disadvantages: manual sleep, complexity with parallel tests. Better to use Testcontainers.
2.2. Testcontainers for Go
Install the library:
go get github.com/testcontainers/testcontainers-goExample with automatic container management:
// repository_test.gopackage repository
import ( "context" "testing" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" "github.com/jackc/pgx/v5/pgxpool")
func TestUserRepository_Save(t *testing.T) { ctx := context.Background()
// Request PostgreSQL container req := testcontainers.ContainerRequest{ Image: "postgres:16-alpine", ExposedPorts: []string{"5432/tcp"}, Env: map[string]string{ "POSTGRES_USER": "test", "POSTGRES_PASSWORD": "test", "POSTGRES_DB": "testdb", }, WaitingFor: wait.ForLog("database system is ready to accept connections"), }
postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req