1. go
  2. /testing
  3. /integration-tests

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