Skip to main content
โšก Calmops

Integration Testing in Go

Integration Testing in Go

Integration testing verifies that multiple components work together correctly. Unlike unit tests that isolate components, integration tests check interactions between components. This guide covers practical integration testing techniques in Go.

Integration Testing Basics

Testing Multiple Components

package main

import (
	"testing"
)

// Components
type UserRepository interface {
	GetUser(id string) (*User, error)
	SaveUser(user *User) error
}

type EmailService interface {
	SendEmail(to, subject, body string) error
}

type UserService struct {
	repo  UserRepository
	email EmailService
}

func (s *UserService) RegisterUser(user *User, email string) error {
	if err := s.repo.SaveUser(user); err != nil {
		return err
	}

	return s.email.SendEmail(email, "Welcome", "Welcome to our service")
}

// Integration test
func TestUserRegistration(t *testing.T) {
	// Create real or test implementations
	repo := NewTestRepository()
	emailService := NewTestEmailService()

	service := &UserService{
		repo:  repo,
		email: emailService,
	}

	user := &User{ID: "1", Name: "Alice"}
	err := service.RegisterUser(user, "[email protected]")

	if err != nil {
		t.Errorf("Unexpected error: %v", err)
	}

	// Verify both components worked together
	savedUser, _ := repo.GetUser("1")
	if savedUser == nil {
		t.Error("User was not saved")
	}

	if len(emailService.SentEmails) != 1 {
		t.Error("Email was not sent")
	}
}

// Test implementations
type TestRepository struct {
	users map[string]*User
}

func NewTestRepository() *TestRepository {
	return &TestRepository{users: make(map[string]*User)}
}

func (r *TestRepository) GetUser(id string) (*User, error) {
	return r.users[id], nil
}

func (r *TestRepository) SaveUser(user *User) error {
	r.users[user.ID] = user
	return nil
}

type TestEmailService struct {
	SentEmails []string
}

func NewTestEmailService() *TestEmailService {
	return &TestEmailService{}
}

func (e *TestEmailService) SendEmail(to, subject, body string) error {
	e.SentEmails = append(e.SentEmails, to)
	return nil
}

Database Integration Testing

Testing with Real Database

package main

import (
	"database/sql"
	"testing"

	_ "github.com/mattn/go-sqlite3"
)

type DatabaseUserRepository struct {
	db *sql.DB
}

func (r *DatabaseUserRepository) GetUser(id string) (*User, error) {
	var user User
	err := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id).
		Scan(&user.ID, &user.Name)
	if err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *DatabaseUserRepository) SaveUser(user *User) error {
	_, err := r.db.Exec("INSERT INTO users (id, name) VALUES (?, ?)", user.ID, user.Name)
	return err
}

func setupTestDB(t *testing.T) *sql.DB {
	db, err := sql.Open("sqlite3", ":memory:")
	if err != nil {
		t.Fatalf("Failed to open database: %v", err)
	}

	// Create tables
	_, err = db.Exec(`
		CREATE TABLE users (
			id TEXT PRIMARY KEY,
			name TEXT NOT NULL
		)
	`)
	if err != nil {
		t.Fatalf("Failed to create table: %v", err)
	}

	return db
}

func TestDatabaseRepository(t *testing.T) {
	db := setupTestDB(t)
	defer db.Close()

	repo := &DatabaseUserRepository{db: db}

	// Test save
	user := &User{ID: "1", Name: "Alice"}
	err := repo.SaveUser(user)
	if err != nil {
		t.Errorf("Failed to save user: %v", err)
	}

	// Test get
	retrieved, err := repo.GetUser("1")
	if err != nil {
		t.Errorf("Failed to get user: %v", err)
	}

	if retrieved.Name != "Alice" {
		t.Errorf("Expected Alice, got %s", retrieved.Name)
	}
}

Using Test Fixtures

package main

import (
	"database/sql"
	"testing"
)

type TestFixture struct {
	DB   *sql.DB
	Repo *DatabaseUserRepository
}

func NewTestFixture(t *testing.T) *TestFixture {
	db := setupTestDB(t)

	// Load test data
	_, err := db.Exec(`
		INSERT INTO users (id, name) VALUES ('1', 'Alice')
		INSERT INTO users (id, name) VALUES ('2', 'Bob')
	`)
	if err != nil {
		t.Fatalf("Failed to load test data: %v", err)
	}

	return &TestFixture{
		DB:   db,
		Repo: &DatabaseUserRepository{db: db},
	}
}

func (f *TestFixture) Cleanup() {
	f.DB.Close()
}

func TestWithFixture(t *testing.T) {
	fixture := NewTestFixture(t)
	defer fixture.Cleanup()

	user, err := fixture.Repo.GetUser("1")
	if err != nil {
		t.Errorf("Failed to get user: %v", err)
	}

	if user.Name != "Alice" {
		t.Errorf("Expected Alice, got %s", user.Name)
	}
}

HTTP Integration Testing

Testing HTTP Handlers

package main

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

type UserHandler struct {
	repo UserRepository
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")
	user, err := h.repo.GetUser(id)
	if err != nil {
		http.Error(w, "User not found", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(user)
}

func TestGetUserHandler(t *testing.T) {
	repo := NewTestRepository()
	repo.SaveUser(&User{ID: "1", Name: "Alice"})

	handler := &UserHandler{repo: repo}

	// Create test request
	req := httptest.NewRequest("GET", "/?id=1", nil)
	w := httptest.NewRecorder()

	handler.GetUser(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("Expected status 200, got %d", w.Code)
	}

	var user User
	json.NewDecoder(w.Body).Decode(&user)

	if user.Name != "Alice" {
		t.Errorf("Expected Alice, got %s", user.Name)
	}
}

Testing Full HTTP Server

package main

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

func setupTestServer(t *testing.T) *httptest.Server {
	repo := NewTestRepository()
	repo.SaveUser(&User{ID: "1", Name: "Alice"})

	handler := &UserHandler{repo: repo}

	mux := http.NewServeMux()
	mux.HandleFunc("/user", handler.GetUser)

	return httptest.NewServer(mux)
}

func TestFullHTTPServer(t *testing.T) {
	server := setupTestServer(t)
	defer server.Close()

	// Make request to test server
	resp, err := http.Get(server.URL + "/user?id=1")
	if err != nil {
		t.Errorf("Failed to make request: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Errorf("Expected status 200, got %d", resp.StatusCode)
	}

	var user User
	json.NewDecoder(resp.Body).Decode(&user)

	if user.Name != "Alice" {
		t.Errorf("Expected Alice, got %s", user.Name)
	}
}

End-to-End Testing

Testing Complete Workflows

package main

import (
	"testing"
)

func TestCompleteUserWorkflow(t *testing.T) {
	// Setup
	repo := NewTestRepository()
	emailService := NewTestEmailService()
	userService := &UserService{repo: repo, email: emailService}

	// Step 1: Register user
	user := &User{ID: "1", Name: "Alice"}
	err := userService.RegisterUser(user, "[email protected]")
	if err != nil {
		t.Fatalf("Failed to register user: %v", err)
	}

	// Step 2: Verify user was saved
	savedUser, err := repo.GetUser("1")
	if err != nil || savedUser == nil {
		t.Fatalf("User was not saved")
	}

	// Step 3: Verify email was sent
	if len(emailService.SentEmails) != 1 {
		t.Fatalf("Email was not sent")
	}

	// Step 4: Update user
	savedUser.Name = "Alice Smith"
	err = repo.SaveUser(savedUser)
	if err != nil {
		t.Fatalf("Failed to update user: %v", err)
	}

	// Step 5: Verify update
	updatedUser, _ := repo.GetUser("1")
	if updatedUser.Name != "Alice Smith" {
		t.Errorf("User was not updated correctly")
	}
}

Best Practices

โœ… Good Practices

// Use test fixtures for setup
fixture := NewTestFixture(t)
defer fixture.Cleanup()

// Test complete workflows
// Verify interactions between components

// Use real implementations when possible
// Only mock external services

// Clean up resources
defer db.Close()

// Use table-driven tests for multiple scenarios
tests := []struct {
	name     string
	input    interface{}
	expected interface{}
}{
	// Test cases
}

โŒ Anti-Patterns

// Don't mock everything
// Test real interactions

// Don't skip cleanup
// Always close resources

// Don't test implementation details
// Test behavior and outcomes

// Don't make tests too slow
// Use in-memory databases when possible

Resources

Summary

Integration testing verifies component interactions:

  • Test multiple components together
  • Use real implementations when possible
  • Test complete workflows
  • Use test fixtures for setup
  • Clean up resources properly
  • Verify behavior, not implementation

With these practices, you can write effective integration tests for Go applications.

Comments