Writing Integration Tests in Go
Integration tests verify that different parts of your application work together correctly. This guide covers how to write and manage integration tests in Go.
Basic Integration Tests
Setting Up Integration Tests
// integration_test.go
package integration
import (
"testing"
"os"
"database/sql"
)
var db *sql.DB
func TestMain(m *testing.M) {
// Setup
var err error
db, err = sql.Open("postgres", os.Getenv("TEST_DB_URL"))
if err != nil {
panic(err)
}
defer db.Close()
// Run migrations
if err := runMigrations(db); err != nil {
panic(err)
}
// Run tests
code := m.Run()
// Cleanup
if err := cleanupDatabase(db); err != nil {
panic(err)
}
os.Exit(code)
}
Database Integration
func TestUserRepository(t *testing.T) {
// Create test data
user := &User{
Name: "John Doe",
Email: "[email protected]",
}
// Setup repository
repo := NewUserRepository(db)
// Test create
createdUser, err := repo.Create(user)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Test read
fetchedUser, err := repo.GetByID(createdUser.ID)
if err != nil {
t.Fatalf("failed to fetch user: %v", err)
}
if fetchedUser.Email != user.Email {
t.Errorf("got email %s, want %s", fetchedUser.Email, user.Email)
}
}
Testing External Services
Mock External API
func TestExternalService(t *testing.T) {
// Start mock server
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/users":
json.NewEncoder(w).Encode(map[string]string{
"id": "123",
"name": "John",
})
default:
http.NotFound(w, r)
}
}))
defer mockServer.Close()
// Configure client with mock server
client := NewClient(mockServer.URL)
service := NewUserService(client)
// Test service integration
user, err := service.GetUser("123")
if err != nil {
t.Fatalf("failed to get user: %v", err)
}
if user.Name != "John" {
t.Errorf("got name %s, want John", user.Name)
}
}
Testing Message Queues
func TestMessageQueue(t *testing.T) {
// Setup test queue
queue := NewTestQueue()
producer := NewProducer(queue)
consumer := NewConsumer(queue)
// Test message flow
message := &Message{
Type: "user_created",
Data: map[string]interface{}{
"user_id": "123",
},
}
// Produce message
if err := producer.Send(message); err != nil {
t.Fatalf("failed to send message: %v", err)
}
// Consume message
received, err := consumer.Receive()
if err != nil {
t.Fatalf("failed to receive message: %v", err)
}
if received.Type != message.Type {
t.Errorf("got message type %s, want %s",
received.Type, message.Type)
}
}
Complex Integration Scenarios
Testing Service Dependencies
type TestEnv struct {
DB *sql.DB
Cache *redis.Client
Queue MessageQueue
HTTPClient *http.Client
cleanup func()
}
func setupTestEnv(t *testing.T) *TestEnv {
t.Helper()
env := &TestEnv{}
// Setup database
db, cleanup := setupTestDB(t)
env.DB = db
// Setup Redis
cache, cleanup2 := setupTestRedis(t)
env.Cache = cache
// Setup message queue
queue, cleanup3 := setupTestQueue(t)
env.Queue = queue
// Setup HTTP client
env.HTTPClient = &http.Client{}
// Combine cleanup functions
env.cleanup = func() {
cleanup3()
cleanup2()
cleanup()
}
return env
}
func TestComplexService(t *testing.T) {
env := setupTestEnv(t)
defer env.cleanup()
service := NewService(
env.DB,
env.Cache,
env.Queue,
env.HTTPClient,
)
// Test complex workflow
result, err := service.ProcessWorkflow(&WorkflowInput{
UserID: "123",
Action: "process_order",
})
if err != nil {
t.Fatalf("workflow failed: %v", err)
}
// Verify database state
order, err := queryOrder(env.DB, result.OrderID)
if err != nil {
t.Fatalf("failed to query order: %v", err)
}
// Verify cache state
cached, err := env.Cache.Get(fmt.Sprintf("order:%s", result.OrderID))
if err != nil {
t.Fatalf("failed to get cache: %v", err)
}
// Verify message was sent
message, err := env.Queue.Receive()
if err != nil {
t.Fatalf("failed to receive message: %v", err)
}
}
Testing Concurrent Operations
func TestConcurrentAccess(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
repo := NewRepository(db)
// Create test data
resource := &Resource{ID: "123", Value: 100}
if err := repo.Create(resource); err != nil {
t.Fatal(err)
}
// Run concurrent updates
var wg sync.WaitGroup
errors := make(chan error, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := repo.Update("123", 10); err != nil {
errors <- err
}
}()
}
// Wait for all operations
wg.Wait()
close(errors)
// Check for errors
for err := range errors {
t.Errorf("concurrent update failed: %v", err)
}
// Verify final state
updated, err := repo.Get("123")
if err != nil {
t.Fatal(err)
}
if updated.Value != 200 {
t.Errorf("got value %d, want 200", updated.Value)
}
}
Best Practices
1. Use Docker for Dependencies
func setupDockerDB(t *testing.T) (*sql.DB, func()) {
pool, err := dockertest.NewPool("")
if err != nil {
t.Fatalf("Could not connect to docker: %s", err)
}
// Run postgres container
resource, err := pool.Run("postgres", "13", []string{
"POSTGRES_PASSWORD=secret",
"POSTGRES_DB=testdb",
})
if err != nil {
t.Fatalf("Could not start resource: %s", err)
}
// Format connection string
connStr := fmt.Sprintf(
"postgres://postgres:secret@localhost:%s/testdb?sslmode=disable",
resource.GetPort("5432/tcp"),
)
// Wait for database to be ready
var db *sql.DB
if err := pool.Retry(func() error {
var err error
db, err = sql.Open("postgres", connStr)
if err != nil {
return err
}
return db.Ping()
}); err != nil {
t.Fatalf("Could not connect to database: %s", err)
}
return db, func() {
db.Close()
pool.Purge(resource)
}
}
2. Manage Test Data
func setupTestData(t *testing.T, db *sql.DB) {
t.Helper()
// Read test data
data, err := os.ReadFile("testdata/setup.sql")
if err != nil {
t.Fatal(err)
}
// Execute setup script
if _, err := db.Exec(string(data)); err != nil {
t.Fatal(err)
}
}
func cleanTestData(t *testing.T, db *sql.DB) {
t.Helper()
// Read cleanup script
data, err := os.ReadFile("testdata/cleanup.sql")
if err != nil {
t.Fatal(err)
}
// Execute cleanup
if _, err := db.Exec(string(data)); err != nil {
t.Fatal(err)
}
}
3. Use Environment Configuration
type TestConfig struct {
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
RedisURL string
QueueURL string
}
func loadTestConfig() TestConfig {
return TestConfig{
DBHost: getEnvOrDefault("TEST_DB_HOST", "localhost"),
DBPort: getEnvOrDefault("TEST_DB_PORT", "5432"),
DBUser: getEnvOrDefault("TEST_DB_USER", "postgres"),
DBPassword: getEnvOrDefault("TEST_DB_PASSWORD", "secret"),
DBName: getEnvOrDefault("TEST_DB_NAME", "testdb"),
RedisURL: getEnvOrDefault("TEST_REDIS_URL", "redis://localhost:6379"),
QueueURL: getEnvOrDefault("TEST_QUEUE_URL", "amqp://localhost:5672"),
}
}
Running Integration Tests
Test Tags
// +build integration
package integration
import "testing"
func TestIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
// Test code...
}
Makefile Configuration
.PHONY: test-integration
test-integration:
docker-compose up -d
go test -tags=integration ./... -timeout 5m
docker-compose down
.PHONY: test-all
test-all: test test-integration
Next Steps
- Learn about HTTP Testing
- Explore Test Coverage
- Study Benchmarking