Go Unit Testing: A Practical Guide for Writing Reliable Tests

Writing reliable tests keeps Go applications stable and maintainable. From basic web servers to complex microservices, good tests are essential for any Go project’s success. Let’s explore practical testing approaches in Go, starting with fundamentals and building up to advanced techniques.

If you’re new to Go programming, start with our Getting Started with Go guide. For everyone ready to learn testing, let’s begin.

Table of Contents

Go’s Testing Package Basics

Go’s standard library includes the testing package, giving you everything needed for effective testing without external frameworks. Here’s what makes it special:

  • Test files end with _test.go
  • Test functions start with Test
  • Simple, clear testing API
  • Built-in benchmarking tools

Your First Go Test

Let’s write a simple test. Start with calculator.go:

package calculator

func Add(a, b int) int {
    return a + b
}

func Subtract(a, b int) int {
    return a - b
}
Code language: JavaScript (javascript)

Create the test file calculator_test.go:

package calculator

import "testing"

func TestAdd(t *testing.T) {
    x, y := 5, 3
    expected := 8
    result := Add(x, y)
    
    if result != expected {
        t.Errorf("Add(%d, %d) = %d; expected %d", x, y, result, expected)
    }
}

func TestSubtract(t *testing.T) {
    x, y := 5, 3
    expected := 2
    result := Subtract(x, y)
    
    if result != expected {
        t.Errorf("Subtract(%d, %d) = %d; expected %d", x, y, result, expected)
    }
}
Code language: JavaScript (javascript)

Smart Test Organization

Keep your tests clean and maintainable with these methods:

Table-Driven Tests

Test multiple scenarios efficiently:

func TestAdd_TableDriven(t *testing.T) {
    tests := []struct {
        name     string
        x, y     int
        expected int
    }{
        {"positive numbers", 5, 3, 8},
        {"negative numbers", -2, -3, -5},
        {"zero values", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.x, tt.y)
            if result != tt.expected {
                t.Errorf("got %d, want %d", result, tt.expected)
            }
        })
    }
}
Code language: JavaScript (javascript)

Helper Functions

Reduce repetition with helper functions:

func assertResult(t *testing.T, got, want int, name string) {
    t.Helper()
    if got != want {
        t.Errorf("%s: got %d, want %d", name, got, want)
    }
}

func TestWithHelper(t *testing.T) {
    assertResult(t, Add(2, 3), 5, "Add(2, 3)")
    assertResult(t, Subtract(5, 2), 3, "Subtract(5, 2)")
}
Code language: JavaScript (javascript)

Testing Complex Types

Here’s how to test more complex functionality:

type Calculator struct {
    precision int
}

func (c *Calculator) DivideWithPrecision(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    result := a / b
    return math.Round(result*math.Pow10(c.precision)) / math.Pow10(c.precision), nil
}
Code language: JavaScript (javascript)

Test complex operations:

func TestDivideWithPrecision(t *testing.T) {
    tests := []struct {
        name           string
        a, b          float64
        precision     int
        expected      float64
        expectedError bool
    }{
        {"simple division", 10, 2, 2, 5.00, false},
        {"division with rounding", 10, 3, 2, 3.33, false},
        {"division by zero", 10, 0, 2, 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            calc := &Calculator{precision: tt.precision}
            result, err := calc.DivideWithPrecision(tt.a, tt.b)

            if tt.expectedError {
                if err == nil {
                    t.Error("expected error, got nil")
                }
                return
            }

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

            if result != tt.expected {
                t.Errorf("got %f, want %f", result, tt.expected)
            }
        })
    }
}
Code language: JavaScript (javascript)

Coverage and Performance

Check test coverage:

go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

Measure performance with benchmarks:

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

func BenchmarkDivideWithPrecision(b *testing.B) {
    calc := &Calculator{precision: 2}
    for i := 0; i < b.N; i++ {
        calc.DivideWithPrecision(10, 3)
    }
}

Testing External Dependencies

Use interfaces and mocks:

type DataStore interface {
    Save(data string) error
    Load() (string, error)
}

type MockDataStore struct {
    savedData string
    err       error
}

func (m *MockDataStore) Save(data string) error {
    if m.err != nil {
        return m.err
    }
    m.savedData = data
    return nil
}

func (m *MockDataStore) Load() (string, error) {
    if m.err != nil {
        return "", m.err
    }
    return m.savedData, nil
}
Code language: PHP (php)

Testing HTTP Handlers

Test web endpoints:

func TestHTTPHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/test", nil)
    w := httptest.NewRecorder()

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "Hello, World!")
    })

    handler.ServeHTTP(w, req)

    resp := w.Result()
    body, _ := io.ReadAll(resp.Body)

    if string(body) != "Hello, World!" {
        t.Errorf("got %q, want %q", string(body), "Hello, World!")
    }
}
Code language: JavaScript (javascript)

Test-Driven Development Tips

  1. Write tests first
  2. Keep tests focused
  3. Use clear test names
  4. Make tests independent
  5. Avoid test dependencies
  6. Update tests with code changes

Wrapping Up

Good tests make Go applications reliable and easier to maintain. Start with simple unit tests, then add more complex patterns as needed. Put these testing methods to work in your next project or add them to existing code.

Need more Go tips? Check our Go Error Handling guide to complement your testing skills.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Share via
Copy link
Powered by Social Snap