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