1. go
  2. /testing
  3. /unit-testing

Writing Unit Tests in Go

Go provides a built-in testing package that makes it easy to write and run unit tests. This guide covers the fundamentals of unit testing in Go.

Basic Test Structure

Writing Your First Test

// math.go
package math

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

// math_test.go
package math

import "testing"

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

Test File Naming

  • Test files should end with _test.go
  • Test functions should start with Test
  • Test function names should be descriptive
  • Place test files in the same package as the code being tested

Test Functions

Test Function Structure

func TestFunctionName(t *testing.T) {
    // Arrange - Set up test data
    input1 := "test"
    input2 := 42
    expected := "expected result"

    // Act - Call the function being tested
    result := FunctionBeingTested(input1, input2)

    // Assert - Check the results
    if result != expected {
        t.Errorf("FunctionBeingTested(%q, %d) = %q; want %q",
            input1, input2, result, expected)
    }
}

Testing Multiple Cases

func TestMultipleCases(t *testing.T) {
    tests := []struct {
        name     string
        input    string
        expected bool
    }{
        {"empty string", "", false},
        {"valid string", "hello", true},
        {"special chars", "!@#", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := ValidateString(tt.input)
            if result != tt.expected {
                t.Errorf("ValidateString(%q) = %v; want %v",
                    tt.input, result, tt.expected)
            }
        })
    }
}

Test Helpers

Creating Helper Functions

func setupTestCase(t *testing.T) func() {
    t.Helper() // Marks this as a helper function

    // Setup code
    db := OpenTestDB()
    
    // Return cleanup function
    return func() {
        db.Close()
    }
}

func TestWithHelper(t *testing.T) {
    cleanup := setupTestCase(t)
    defer cleanup()
    
    // Test code here
}

Assert Functions

func assertEqual(t *testing.T, got, want interface{}) {
    t.Helper()
    if got != want {
        t.Errorf("got %v; want %v", got, want)
    }
}

func assertNoError(t *testing.T, err error) {
    t.Helper()
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
}

Testing Errors

Error Testing

func TestErrorHandling(t *testing.T) {
    // Test for expected error
    _, err := ParseConfig("invalid")
    if err == nil {
        t.Error("expected error, got nil")
    }

    // Test for specific error
    if !errors.Is(err, ErrInvalidConfig) {
        t.Errorf("got error %v; want %v", err, ErrInvalidConfig)
    }

    // Test error message
    want := "invalid configuration"
    if got := err.Error(); got != want {
        t.Errorf("error message = %q; want %q", got, want)
    }
}

Testing Panics

func TestPanic(t *testing.T) {
    defer func() {
        if r := recover(); r == nil {
            t.Error("expected panic, but function did not panic")
        }
    }()

    // Function that should panic
    DangerousFunction()
}

Setup and Teardown

Test Main

func TestMain(m *testing.M) {
    // Setup code
    setupTestEnvironment()

    // Run tests
    code := m.Run()

    // Cleanup code
    cleanupTestEnvironment()

    // Exit with test result code
    os.Exit(code)
}

Individual Test Setup

type TestSuite struct {
    db     *sql.DB
    server *httptest.Server
}

func setupTest(t *testing.T) *TestSuite {
    t.Helper()
    
    suite := &TestSuite{
        db:     setupTestDB(t),
        server: setupTestServer(t),
    }
    
    t.Cleanup(func() {
        suite.db.Close()
        suite.server.Close()
    })
    
    return suite
}

Best Practices

  1. Keep Tests Focused

    // Good - Single focused test
    func TestUserValidation(t *testing.T) {
        user := User{Name: "", Age: -1}
        err := user.Validate()
        if err == nil {
            t.Error("expected validation error")
        }
    }
    
  2. Use Clear Error Messages

    // Good - Clear error message
    if got != want {
        t.Errorf("ProcessString(%q) = %q; want %q", input, got, want)
    }
    
    // Bad - Unclear error message
    if got != want {
        t.Error("wrong result")
    }
    
  3. Test Edge Cases

    func TestDivide(t *testing.T) {
        tests := []struct {
            name        string
            a, b       float64
            want       float64
            wantErr    bool
        }{
            {"valid division", 10, 2, 5, false},
            {"zero division", 1, 0, 0, true},
            {"negative numbers", -10, -2, 5, false},
            {"very large numbers", 1e308, 2, 5e307, false},
        }
        // ... test implementation
    }
    
  4. Use Subtests for Organization

    func TestUser(t *testing.T) {
        t.Run("validation", func(t *testing.T) {
            // Test user validation
        })
        
        t.Run("serialization", func(t *testing.T) {
            // Test user serialization
        })
    }
    

Running Tests

# Run all tests in current package
go test

# Run tests with coverage
go test -cover

# Run specific test
go test -run TestFunctionName

# Run tests verbosely
go test -v

# Run tests in all subdirectories
go test ./...

Next Steps