Integration Testing in Go

Integration Testing in Go

Integration testing checks how different parts of your application work together. Unlike unit tests that focus on individual functions, integration tests verify that components interact correctly, often involving external dependencies like databases or APIs.

Setting Up Integration Tests

Integration tests usually go in a separate package or use build tags to avoid running them with regular unit tests.

integration_test.go
// +build integration

package main

import "testing"

// This file will only be included when running tests with -tags=integration

Or put them in a separate directory:

project/
├── main.go
├── handlers/
│   └── user.go
├── models/
│   └── user.go
├── integration/
│   └── user_test.go

Testing with Databases

A common integration test involves testing database operations:

integration/user_test.go
package integration

import (
    "database/sql"
    "os"
    "testing"
    
    _ "github.com/lib/pq" // PostgreSQL driver
)

func TestMain(m *testing.M) {
    // Setup test database
    db, err := sql.Open("postgres", os.Getenv("TEST_DATABASE_URL"))
    if err != nil {
        panic(err)
    }
    defer db.Close()
    
    // Run migrations or setup schema
    _, err = db.Exec(`
        CREATE TABLE IF NOT EXISTS users (
            id SERIAL PRIMARY KEY,
            name TEXT NOT NULL,
            email TEXT UNIQUE NOT NULL
        )
    `)
    if err != nil {
        panic(err)
    }
    
    // Store db in global variable or pass to tests
    testDB = db
    
    // Run tests
    code := m.Run()
    
    // Cleanup
    db.Exec("DROP TABLE users")
    
    os.Exit(code)
}

var testDB *sql.DB

func TestCreateUser(t *testing.T) {
    user := User{Name: "John Doe", Email: "[email protected]"}
    
    err := CreateUser(testDB, user)
    if err != nil {
        t.Fatalf("CreateUser failed: %v", err)
    }
    
    // Verify user was created
    savedUser, err := GetUserByEmail(testDB, user.Email)
    if err != nil {
        t.Fatalf("GetUserByEmail failed: %v", err)
    }
    
    if savedUser.Name != user.Name {
        t.Errorf("Expected name %s, got %s", user.Name, savedUser.Name)
    }
}

Testing HTTP Handlers

For web applications, test the full HTTP request/response cycle:

integration/http_test.go
package integration

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestUserAPI(t *testing.T) {
    // Setup test server
    router := setupRouter() // Your router setup function
    server := httptest.NewServer(router)
    defer server.Close()
    
    // Test creating a user
    userData := map[string]string{
        "name":  "Jane Doe",
        "email": "[email protected]",
    }
    
    jsonData, _ := json.Marshal(userData)
    
    resp, err := http.Post(server.URL+"/users", "application/json", bytes.NewBuffer(jsonData))
    if err != nil {
        t.Fatalf("POST request failed: %v", err)
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusCreated {
        t.Errorf("Expected status 201, got %d", resp.StatusCode)
    }
    
    var response map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&response)
    
    if response["name"] != userData["name"] {
        t.Errorf("Expected name %s, got %v", userData["name"], response["name"])
    }
}

Testing External APIs

When your code calls external services:

integration/api_test.go
package integration

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestExternalAPIIntegration(t *testing.T) {
    // Mock external API
    externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"status": "success", "data": {"id": 123}}`))
    }))
    defer externalServer.Close()
    
    // Configure your client to use the mock server
    client := NewAPIClient()
    client.BaseURL = externalServer.URL
    
    // Test your function that calls the external API
    result, err := client.FetchData()
    if err != nil {
        t.Fatalf("FetchData failed: %v", err)
    }
    
    if result.ID != 123 {
        t.Errorf("Expected ID 123, got %d", result.ID)
    }
}

Using Test Containers

For testing with real databases or services, use test containers:

integration/container_test.go
package integration

import (
    "database/sql"
    "testing"
    
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
)

func TestWithPostgres(t *testing.T) {
    ctx := context.Background()
    
    // Start PostgreSQL container
    req := testcontainers.ContainerRequest{
        Image:        "postgres:13",
        ExposedPorts: []string{"5432/tcp"},
        Env: map[string]string{
            "POSTGRES_PASSWORD": "password",
            "POSTGRES_DB":       "testdb",
        },
        WaitingFor: wait.ForLog("database system is ready to accept connections"),
    }
    
    postgresC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    if err != nil {
        t.Fatal(err)
    }
    defer postgresC.Terminate(ctx)
    
    // Get connection details
    host, _ := postgresC.Host(ctx)
    port, _ := postgresC.MappedPort(ctx, "5432")
    dbURL := fmt.Sprintf("postgres://postgres:password@%s:%s/testdb?sslmode=disable", host, port)
    
    // Connect to database
    db, err := sql.Open("postgres", dbURL)
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()
    
    // Run your integration tests with the real database
    // ...
}

Running Integration Tests

  • go test -tags=integration: Run tests with integration tag
  • go test ./integration/...: Run tests in integration directory
  • go test -short: Skip long-running integration tests (use t.Short() in code)

Best Practices

  1. Isolate tests: Each test should be independent
  2. Use realistic data: Test with data that matches production
  3. Clean up after tests: Remove test data to avoid interference
  4. Handle timeouts: Integration tests can be slow
  5. Mock when possible: Use mocks for external services when practical
  6. Parallel execution: Be careful with shared resources

When to Use Integration Tests

  • Testing database operations
  • API endpoints
  • External service integrations
  • End-to-end workflows
  • Performance under load

Remember, integration tests complement unit tests. Use unit tests for logic verification and integration tests for component interaction.

For more on unit testing, see our unit testing tutorial. If you’re working with web servers, check out building web servers.

Last updated on