Lab 12: Testing

Objective

Write unit tests, table-driven tests, benchmarks, and fuzz tests using Go's built-in testing package. Use httptest for HTTP handler testing.

Time

30 minutes

Prerequisites

  • Lab 11 (HTTP & REST)

Tools

  • Docker image: zchencow/innozverse-go:latest


Lab Instructions

Step 1: Basic Tests with testing.T

docker run --rm zchencow/innozverse-go:latest sh -c '
mkdir -p /tmp/gotest/mathutil
cat > /tmp/gotest/mathutil/math.go << "GOEOF"
package mathutil

import "errors"

func Add(a, b int) int      { return a + b }
func Subtract(a, b int) int { return a - b }
func Multiply(a, b int) int { return a * b }

func Divide(a, b float64) (float64, error) {
    if b == 0 { return 0, errors.New("division by zero") }
    return a / b, nil
}

func Factorial(n int) int {
    if n < 0   { return -1 }
    if n == 0  { return 1 }
    return n * Factorial(n-1)
}

func IsPrime(n int) bool {
    if n < 2 { return false }
    for i := 2; i*i <= n; i++ {
        if n%i == 0 { return false }
    }
    return true
}
GOEOF

cat > /tmp/gotest/mathutil/math_test.go << "GOEOF"
package mathutil

import (
    "testing"
    "math"
)

// Basic test
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2,3) = %d, want 5", result)
    }
}

// Table-driven test — the Go way
func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a, b    float64
        want    float64
        wantErr bool
    }{
        {"normal",      10, 2, 5, false},
        {"decimal",     7, 2, 3.5, false},
        {"divide by zero", 5, 0, 0, true},
        {"negative",    -10, 2, -5, false},
    }
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            got, err := Divide(tc.a, tc.b)
            if tc.wantErr {
                if err == nil { t.Error("expected error, got nil") }
                return
            }
            if err != nil { t.Fatalf("unexpected error: %v", err) }
            if math.Abs(got - tc.want) > 0.001 {
                t.Errorf("Divide(%.1f,%.1f) = %.4f, want %.4f", tc.a, tc.b, got, tc.want)
            }
        })
    }
}

func TestFactorial(t *testing.T) {
    cases := map[int]int{0: 1, 1: 1, 5: 120, 10: 3628800}
    for n, want := range cases {
        if got := Factorial(n); got != want {
            t.Errorf("Factorial(%d) = %d, want %d", n, got, want)
        }
    }
}

func TestIsPrime(t *testing.T) {
    primes    := []int{2, 3, 5, 7, 11, 13, 17, 19, 23}
    notPrimes := []int{0, 1, 4, 6, 8, 9, 10, 15, 25}
    for _, n := range primes {
        if !IsPrime(n) { t.Errorf("IsPrime(%d) = false, want true", n) }
    }
    for _, n := range notPrimes {
        if IsPrime(n) { t.Errorf("IsPrime(%d) = true, want false", n) }
    }
}

// Benchmark
func BenchmarkFactorial(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Factorial(20)
    }
}
GOEOF

cd /tmp/gotest/mathutil
go mod init mathutil 2>/dev/null || true
go test -v ./...
'

💡 Table-driven tests are the idiomatic Go testing pattern. Instead of writing one TestXxx per case, define a slice of structs with inputs and expected outputs, then loop with t.Run(name, fn). This gives you named subtests, parallel execution (t.Parallel()), and easy addition of new cases.

📸 Verified Output:


Step 2: HTTP Handler Tests

📸 Verified Output:


Steps 3–8: Mocks, Benchmarks, TestMain, Helpers, Parallel, Capstone

📸 Verified Output:


Summary

Pattern
Syntax
Use case

Basic test

func TestXxx(t *testing.T)

Unit testing

Table-driven

for _, tc := range tests { t.Run(...) }

Multiple cases

Subtests

t.Run("name", func(t *testing.T) {})

Named test cases

Parallel

t.Parallel()

Speed up independent tests

Benchmark

func BenchmarkXxx(b *testing.B)

Performance measurement

HTTP test

httptest.NewRequest + NewRecorder

Handler testing

Helper

t.Helper()

Better error location reporting

Further Reading

Last updated