Loading...
Loading...

Go Testing Tutorial

Testing is a critical part of Go development. The standard library provides powerful tools for writing and running tests. This tutorial covers everything from basic tests to advanced techniques.

1. Testing Basics

1.1 Test Files and Functions

Go tests follow specific conventions:

  • Test files end with _test.go
  • Test functions start with Test
  • Use testing.T for test control
// math.go
func Add(a, b int) int {
    return a + b
}

// math_test.go
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Expected 5, got %d", result)
    }
}

2. Table-Driven Tests

2.1 Basic Table Tests

Efficient way to test multiple scenarios:

func TestMultiply(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 6},
        {"negative numbers", -1, -1, 1},
        {"zero case", 0, 5, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Multiply(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("%s: expected %d, got %d", 
                    tt.name, tt.expected, result)
            }
        })
    }
}

2.2 Subtests and Parallel Execution

Run tests in parallel for speed:

func TestDivide(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
        wantErr  bool
    }{
        {"normal division", 6, 3, 2, false},
        {"divide by zero", 1, 0, 0, true},
    }

    for _, tt := range tests {
        tt := tt // capture range variable
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            result, err := Divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Fatalf("unexpected error: %v", err)
            }
            if !tt.wantErr && result != tt.expected {
                t.Errorf("expected %d, got %d", tt.expected, result)
            }
        })
    }
}

3. Test Utilities

3.1 Test Helpers

Create reusable test utilities:

func testHelper(t *testing.T, input string, expected int) {
    t.Helper() // Marks this as a helper function
    result := Process(input)
    if result != expected {
        t.Errorf("for input %q, expected %d, got %d", 
            input, expected, result)
    }
}

func TestProcess(t *testing.T) {
    testHelper(t, "abc", 3)
    testHelper(t, "", 0)
}

3.2 Golden Files

Compare output against known-good "golden" files:

func TestTemplate(t *testing.T) {
    data := struct{ Name string }{Name: "Alice"}
    result := renderTemplate(data)
    
    golden := filepath.Join("testdata", t.Name()+".golden")
    if *update {
        ioutil.WriteFile(golden, []byte(result), 0644)
    }
    
    expected, _ := ioutil.ReadFile(golden)
    if result != string(expected) {
        t.Errorf("rendered template does not match golden file")
    }
}

4. HTTP Testing

4.1 Testing HTTP Handlers

Use net/http/httptest package:

func TestHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/test", nil)
    w := httptest.NewRecorder()
    
    handler(w, req)
    
    resp := w.Result()
    if resp.StatusCode != http.StatusOK {
        t.Errorf("expected status 200, got %d", resp.StatusCode)
    }
    
    body, _ := ioutil.ReadAll(resp.Body)
    if string(body) != "expected response" {
        t.Errorf("unexpected response body: %s", body)
    }
}

4.2 Testing Middleware

Test middleware in isolation:

func TestAuthMiddleware(t *testing.T) {
    tests := []struct {
        name       string
        authHeader string
        wantStatus int
    }{
        {"valid token", "Bearer valid", 200},
        {"invalid token", "Bearer invalid", 401},
        {"missing token", "", 401},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest("GET", "/", nil)
            if tt.authHeader != "" {
                req.Header.Set("Authorization", tt.authHeader)
            }
            
            w := httptest.NewRecorder()
            next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                w.WriteHeader(http.StatusOK)
            })
            
            authMiddleware(next).ServeHTTP(w, req)
            
            if w.Code != tt.wantStatus {
                t.Errorf("expected status %d, got %d", 
                    tt.wantStatus, w.Code)
            }
        })
    }
}

5. Advanced Testing Techniques

5.1 Mocking and Interfaces

Use interfaces for testable dependencies:

type Database interface {
    GetUser(id int) (*User, error)
}

type RealDB struct{ /* ... */ }
func (db *RealDB) GetUser(id int) (*User, error) { /* ... */ }

type MockDB struct{ /* ... */ }
func (db *MockDB) GetUser(id int) (*User, error) {
    return &User{ID: id, Name: "Test User"}, nil
}

func TestService(t *testing.T) {
    mockDB := &MockDB{}
    service := NewService(mockDB)
    
    user, err := service.GetUser(1)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if user.Name != "Test User" {
        t.Errorf("unexpected user name: %s", user.Name)
    }
}

5.2 Test Fixtures

Load test data from files:

func loadFixture(t *testing.T, name string) []byte {
    t.Helper()
    path := filepath.Join("testdata", name)
    data, err := ioutil.ReadFile(path)
    if err != nil {
        t.Fatalf("failed to load fixture %s: %v", name, err)
    }
    return data
}

func TestWithFixtures(t *testing.T) {
    input := loadFixture(t, "input.json")
    expected := loadFixture(t, "expected.json")
    
    result := Process(input)
    if !bytes.Equal(result, expected) {
        t.Errorf("result does not match expected output")
    }
}

6. Benchmarking

6.1 Writing Benchmarks

Benchmark functions start with Benchmark:

func BenchmarkSort(b *testing.B) {
    data := generateTestData(1000)
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        Sort(data)
    }
}

6.2 Advanced Benchmarking

Run benchmarks with different inputs:

func benchmarkSort(b *testing.B, size int) {
    data := generateTestData(size)
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        Sort(data)
    }
}

func BenchmarkSort100(b *testing.B)   { benchmarkSort(b, 100) }
func BenchmarkSort1000(b *testing.B)  { benchmarkSort(b, 1000) }
func BenchmarkSort10000(b *testing.B) { benchmarkSort(b, 10000) }

7. Best Practices

7.1 Test Organization

  • Keep tests with code: foo.go and foo_test.go
  • Test package: Use package_test for black-box testing
  • Test directories: Use testdata/ for fixtures

7.2 Common Pitfalls

// ❌ Bad: Not using t.Helper() in test helpers
func badHelper(t *testing.T, input string) {
    // Without t.Helper(), failures point to this line
    if len(input) == 0 {
        t.Error("empty input")
    }
}

// ✅ Good: Using t.Helper() correctly
func goodHelper(t *testing.T, input string) {
    t.Helper() // Failures will point to the calling test
    if len(input) == 0 {
        t.Error("empty input")
    }
}

// ❌ Bad: Not cleaning up resources
func TestWithTempFile(t *testing.T) {
    f, _ := ioutil.TempFile("", "test")
    // Forgot to remove temp file
}

// ✅ Good: Proper cleanup
func TestWithTempFile(t *testing.T) {
    f, _ := ioutil.TempFile("", "test")
    defer os.Remove(f.Name()) // Clean up after test
    // Test code...
}
0 Interaction
0 Views
Views
0 Likes
×
×
×
🍪 CookieConsent@Ptutorials:~

Welcome to Ptutorials

$ Allow cookies on this site ? (y/n)

top-home