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.
// +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.goTesting with Databases
A common integration test involves testing database operations:
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:
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:
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:
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 taggo test ./integration/...: Run tests in integration directorygo test -short: Skip long-running integration tests (use t.Short() in code)
Best Practices
- Isolate tests: Each test should be independent
- Use realistic data: Test with data that matches production
- Clean up after tests: Remove test data to avoid interference
- Handle timeouts: Integration tests can be slow
- Mock when possible: Use mocks for external services when practical
- 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.