From agent-skills
Write integration tests in Go using testcontainers-go with real databases, caches, and message queues in Docker containers. Covers PostgreSQL, MySQL, Redis, RabbitMQ, Kafka, and custom containers with idiomatic Go testing patterns.
npx claudepluginhub baotoq/agent-skills --plugin agent-skillsThis skill uses the workspace's default tool permissions.
- Writing integration tests that need real infrastructure (databases, caches, message queues)
Guides writing Go integration tests with Testcontainers using Docker containers for databases (PostgreSQL, MySQL, Redis, MongoDB), queues (Kafka, RabbitMQ), and services. Covers modules, networking, cleanup, wait strategies, and CI/CD.
Sets up integration tests across databases, APIs, and message queues using Testcontainers, with DB seeding, cleanup strategies, and Docker dependencies.
Writes .NET integration tests using TestContainers and xUnit with real Docker containers for databases, Redis, RabbitMQ, avoiding mocks for production-like testing.
Share bugs, ideas, or general feedback.
testcontainers.CleanupContainer(t, ctr) handles lifecyclet.Helper(), t.Cleanup(), subtestscontext.Context to all container operationsLoad detailed guidance based on context:
| Topic | Reference | Load When |
|---|---|---|
| Advanced Patterns | references/advanced-patterns.md | Multi-container networks, Kafka, snapshots, TestMain, CI/CD |
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/postgres
go get github.com/testcontainers/testcontainers-go/modules/mysql
go get github.com/testcontainers/testcontainers-go/modules/redis
go get github.com/testcontainers/testcontainers-go/modules/rabbitmq
go get github.com/testcontainers/testcontainers-go/modules/kafka
// BAD: Mocking a database - doesn't test real SQL behavior
type mockDB struct{}
func (m *mockDB) GetUser(id string) (*User, error) {
return &User{ID: id, Name: "Alice"}, nil // No real query executed
}
// GOOD: Test against a real database with testcontainers
func TestGetUser(t *testing.T) {
ctx := context.Background()
ctr, err := postgres.Run(ctx, "postgres:16-alpine",
postgres.WithDatabase("testdb"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
postgres.BasicWaitStrategies(),
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
connStr, err := ctr.ConnectionString(ctx)
require.NoError(t, err)
db, err := sql.Open("pgx", connStr)
require.NoError(t, err)
defer db.Close()
// Test real SQL queries, constraints, and behavior
}
package repository_test
import (
"context"
"database/sql"
"testing"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
_ "github.com/jackc/pgx/v5/stdlib"
)
func setupPostgres(t *testing.T) *sql.DB {
t.Helper()
ctx := context.Background()
ctr, err := postgres.Run(ctx, "postgres:16-alpine",
postgres.WithDatabase("testdb"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
postgres.BasicWaitStrategies(),
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
connStr, err := ctr.ConnectionString(ctx)
require.NoError(t, err)
db, err := sql.Open("pgx", connStr)
require.NoError(t, err)
t.Cleanup(func() { db.Close() })
// Run migrations
_, err = db.ExecContext(ctx, `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)`)
require.NoError(t, err)
return db
}
func TestUserRepository(t *testing.T) {
db := setupPostgres(t)
repo := NewUserRepository(db)
t.Run("Create", func(t *testing.T) {
err := repo.Create(context.Background(), &User{Name: "Alice", Email: "alice@test.com"})
require.NoError(t, err)
})
t.Run("GetByEmail", func(t *testing.T) {
user, err := repo.GetByEmail(context.Background(), "alice@test.com")
require.NoError(t, err)
require.Equal(t, "Alice", user.Name)
})
}
func setupMySQL(t *testing.T) *sql.DB {
t.Helper()
ctx := context.Background()
ctr, err := mysql.Run(ctx, "mysql:8.0.36",
mysql.WithDatabase("testdb"),
mysql.WithUsername("test"),
mysql.WithPassword("test"),
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
connStr, err := ctr.ConnectionString(ctx)
require.NoError(t, err)
db, err := sql.Open("mysql", connStr)
require.NoError(t, err)
t.Cleanup(func() { db.Close() })
return db
}
package cache_test
import (
"context"
"testing"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
tcredis "github.com/testcontainers/testcontainers-go/modules/redis"
)
func setupRedis(t *testing.T) *redis.Client {
t.Helper()
ctx := context.Background()
ctr, err := tcredis.Run(ctx, "redis:7")
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
endpoint, err := ctr.Endpoint(ctx, "")
require.NoError(t, err)
client := redis.NewClient(&redis.Options{Addr: endpoint})
t.Cleanup(func() { client.Close() })
return client
}
func TestCacheService(t *testing.T) {
client := setupRedis(t)
cache := NewCacheService(client)
ctx := context.Background()
t.Run("SetAndGet", func(t *testing.T) {
err := cache.Set(ctx, "key1", "value1", 0)
require.NoError(t, err)
val, err := cache.Get(ctx, "key1")
require.NoError(t, err)
require.Equal(t, "value1", val)
})
t.Run("GetMiss", func(t *testing.T) {
_, err := cache.Get(ctx, "nonexistent")
require.ErrorIs(t, err, ErrCacheMiss)
})
}
package messaging_test
import (
"context"
"testing"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/rabbitmq"
)
func setupRabbitMQ(t *testing.T) *amqp.Connection {
t.Helper()
ctx := context.Background()
ctr, err := rabbitmq.Run(ctx, "rabbitmq:3-management-alpine",
rabbitmq.WithAdminUsername("guest"),
rabbitmq.WithAdminPassword("guest"),
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
endpoint, err := ctr.AmqpURL(ctx)
require.NoError(t, err)
conn, err := amqp.Dial(endpoint)
require.NoError(t, err)
t.Cleanup(func() { conn.Close() })
return conn
}
func TestPublishAndConsume(t *testing.T) {
conn := setupRabbitMQ(t)
ctx := context.Background()
ch, err := conn.Channel()
require.NoError(t, err)
defer ch.Close()
q, err := ch.QueueDeclare("test-queue", false, true, false, false, nil)
require.NoError(t, err)
// Publish
body := []byte("hello")
err = ch.PublishWithContext(ctx, "", q.Name, false, false, amqp.Publishing{
ContentType: "text/plain",
Body: body,
})
require.NoError(t, err)
// Consume
msgs, err := ch.Consume(q.Name, "", true, false, false, false, nil)
require.NoError(t, err)
msg := <-msgs
require.Equal(t, body, msg.Body)
}
For services without a dedicated module:
func setupMinio(t *testing.T) string {
t.Helper()
ctx := context.Background()
ctr, err := testcontainers.Run(ctx, "minio/minio:latest",
testcontainers.WithExposedPorts("9000/tcp"),
testcontainers.WithEnv(map[string]string{
"MINIO_ROOT_USER": "minioadmin",
"MINIO_ROOT_PASSWORD": "minioadmin",
}),
testcontainers.WithCmd("server", "/data"),
testcontainers.WithWaitStrategy(
wait.ForListeningPort("9000/tcp"),
),
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
endpoint, err := ctr.Endpoint(ctx, "")
require.NoError(t, err)
return endpoint
}
Combine testcontainers with Go's table-driven test pattern:
func TestOrderRepository_Create(t *testing.T) {
db := setupPostgres(t)
repo := NewOrderRepository(db)
tests := []struct {
name string
order Order
wantErr bool
}{
{
name: "valid order",
order: Order{CustomerID: "CUST1", Total: 99.99},
},
{
name: "missing customer ID",
order: Order{Total: 50.00},
wantErr: true,
},
{
name: "negative total",
order: Order{CustomerID: "CUST2", Total: -10.00},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := repo.Create(context.Background(), &tt.order)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotZero(t, tt.order.ID)
})
}
}
Reuse a single container across all tests in a package for speed:
package repository_test
var testDB *sql.DB
func TestMain(m *testing.M) {
ctx := context.Background()
ctr, err := postgres.Run(ctx, "postgres:16-alpine",
postgres.WithDatabase("testdb"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
postgres.BasicWaitStrategies(),
)
if err != nil {
log.Fatal(err)
}
connStr, err := ctr.ConnectionString(ctx)
if err != nil {
log.Fatal(err)
}
testDB, err = sql.Open("pgx", connStr)
if err != nil {
log.Fatal(err)
}
// Run migrations
runMigrations(testDB)
code := m.Run()
testDB.Close()
testcontainers.TerminateContainer(ctr)
os.Exit(code)
}
func TestWithSharedDB(t *testing.T) {
// Use testDB directly - container is shared across all tests
repo := NewUserRepository(testDB)
// ...
}
testcontainers.CleanupContainer(t, ctr) - Automatic cleanup tied to test lifecyclet.Helper() - Mark setup functions as helpers for clean stack tracest.Cleanup() - Register deferred cleanup for connections and clientspostgres.Run(), tcredis.Run() over generic containersEndpoint() or ConnectionString()TestMain - One container per package, not per testcontext.Background() to container operationsgo test -race//go:build integration//go:build integration
package repository_test
// These tests only run with: go test -tags=integration ./...
| Issue | Solution |
|---|---|
| Container startup timeout | Increase Docker resource limits or use lightweight images (alpine) |
| Port conflicts | Always use random ports via Endpoint() - never fixed ports |
| Tests fail in CI | Ensure CI runner has Docker (ubuntu-latest on GitHub Actions) |
| Slow test suite | Share containers via TestMain instead of per-test containers |
| Flaky connection | Use module-provided wait strategies (postgres.BasicWaitStrategies()) |
| Leaked containers | Always call testcontainers.CleanupContainer(t, ctr) immediately after Run |