Integration Testing Go with Docker: Advanced Level
Integration testing is a critically important stage in developing reliable microservices in Go. Isolated unit tests do not guarantee that your application correctly interacts with external dependencies: databases, message queues, caches, and third-party APIs. Docker allows you to spin up real instances of these services in a test environment, ensuring maximum test reliability. In this article, we will explore advanced techniques for integration testing Go applications using Docker.
Why Docker is Important for Integration Tests in Go?
The traditional approach with mocks and stubs only checks the logic of your code, but not the behavior in conjunction with a real DBMS or broker. Docker solves this problem:
- Environment Reproducibility: tests run in identical conditions locally and in CI/CD.
- Isolation: each test suite can have its own instance of the service.
- No "Magic": you are testing exactly the driver/client you use in production.
However, manually managing containers via os/exec or shell scripts quickly becomes unstable. Libraries such as testcontainers-go come to the rescue.
Testcontainers Library for Go: Installation and Basic Example
testcontainers-go is a Go wrapper that programmatically manages the lifecycle of Docker containers. It automatically pulls images, starts, waits for readiness, and cleans up containers after the test.
Installation
go get github.com/testcontainers/testcontainers-gogo get github.com/testcontainers/testcontainers-go/modules/postgresBasic Example: Testing with PostgreSQL
package integration
import ( "context" "database/sql" "testing" "time"
"github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" _ "github.com/lib/pq")
func TestPostgresConnection(t *testing.T) { ctx := context.Background()
// 1. Start the PostgreSQL container pgContainer, err := postgres.RunContainer(ctx, testcontainers.WithImage("postgres:15"), postgres.WithDatabase("testdb"), postgres.WithUsername("user"), postgres.WithPassword("password"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(30*time.Second)), ) if err != nil { t.Fatal(err) } defer pgContainer.Terminate(ctx) // Automatic cleanup
// 2. Get the connection string connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") if err != nil { t.Fatal(err) }
// 3. Connect and execute a query db, err := sql.Open("postgres", connStr) if err != nil { t.Fatal(err) } defer db.Close()
if err := db.Ping(); err != nil { t.Fatal("Failed to connect to the database:", err) }
// Verify we can create a table and insert data _, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT)`) if err != nil { t.Fatal(err) } t.Log("Integration test with PostgreSQL passed successfully!")}Advanced Techniques: Parallel Execution and Custom Wait Strategies
When there are many tests, running containers sequentially for each test slows down the execution. We use Go's parallelism and shared containers.
Parallel Tests with a Shared Container
Create one container for the entire package using TestMain:
package integrat