Skip to main content

Unit Testing in Go

Created: May 8, 2026 Larry Qu 6 min read

Testing is a first-class citizen in Go. The language includes a built-in testing package and encourages test-driven development. This guide covers everything you need to know about writing effective unit tests in Go. For more context, see Go Installation Guide, Go Ecosystem Overview, Go Best Practices.

Getting Started with Testing

Basic Test Structure

package main

import (
	"testing"
)

// Function to test
func Add(a, b int) int {
	return a + b
}

// Test function
func TestAdd(t *testing.T) {
	result := Add(2, 3)
	expected := 5

	if result != expected {
		t.Errorf("Add(2, 3) = %d; want %d", result, expected)
	}
}

func TestAddNegative(t *testing.T) {
	result := Add(-2, -3)
	expected := -5

	if result != expected {
		t.Errorf("Add(-2, -3) = %d; want %d", result, expected)
	}
}

Running Tests

# Run all tests in current package
go test

# Run with verbose output
go test -v

# Run specific test
go test -run TestAdd

# Run with coverage
go test -cover

# Generate coverage report
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

Test Patterns

Table-Driven Tests

package main

import (
	"testing"
)

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

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

Subtests

package main

import (
	"testing"
)

func TestMath(t *testing.T) {
	t.Run("addition", func(t *testing.T) {
		result := Add(2, 3)
		if result != 5 {
			t.Errorf("Add failed: got %d, want 5", result)
		}
	})

	t.Run("subtraction", func(t *testing.T) {
		result := 5 - 3
		if result != 2 {
			t.Errorf("Subtraction failed: got %d, want 2", result)
		}
	})

	t.Run("multiplication", func(t *testing.T) {
		result := 3 * 4
		if result != 12 {
			t.Errorf("Multiplication failed: got %d, want 12", result)
		}
	})
}

Test Helpers and Utilities

Helper Functions

package main

import (
	"testing"
)

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

func TestWithHelper(t *testing.T) {
	result := Add(2, 3)
	assertEqual(t, result, 5)
}

Setup and Teardown

package main

import (
	"testing"
)

func TestWithSetup(t *testing.T) {
	// Setup
	data := []int{1, 2, 3, 4, 5}

	// Test
	if len(data) != 5 {
		t.Errorf("Expected 5 elements, got %d", len(data))
	}

	// Teardown (if needed)
	data = nil
}

func TestWithCleanup(t *testing.T) {
	// Setup
	resource := "test resource"

	// Register cleanup
	t.Cleanup(func() {
		// Cleanup code
		resource = ""
	})

	// Test
	if resource != "test resource" {
		t.Errorf("Resource not initialized")
	}
}

Testing Different Scenarios

Testing Errors

package main

import (
	"errors"
	"testing"
)

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

func TestDivideError(t *testing.T) {
	_, err := Divide(10, 0)
	if err == nil {
		t.Error("Expected error for division by zero")
	}

	if err.Error() != "division by zero" {
		t.Errorf("Wrong error message: %v", err)
	}
}

func TestDivideSuccess(t *testing.T) {
	result, err := Divide(10, 2)
	if err != nil {
		t.Errorf("Unexpected error: %v", err)
	}

	if result != 5 {
		t.Errorf("Expected 5, got %d", result)
	}
}

Testing Interfaces

package main

import (
	"testing"
)

type Reader interface {
	Read() string
}

type MockReader struct {
	data string
}

func (m *MockReader) Read() string {
	return m.data
}

func ProcessReader(r Reader) string {
	return "Processed: " + r.Read()
}

func TestInterface(t *testing.T) {
	mock := &MockReader{data: "test"}
	result := ProcessReader(mock)

	expected := "Processed: test"
	if result != expected {
		t.Errorf("Expected %q, got %q", expected, result)
	}
}

Testing Concurrency

package main

import (
	"sync"
	"testing"
	"time"
)

func TestConcurrency(t *testing.T) {
	var wg sync.WaitGroup
	results := make([]int, 0)
	var mu sync.Mutex

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			mu.Lock()
			results = append(results, n)
			mu.Unlock()
		}(i)
	}

	wg.Wait()

	if len(results) != 10 {
		t.Errorf("Expected 10 results, got %d", len(results))
	}
}

func TestTimeout(t *testing.T) {
	done := make(chan bool)

	go func() {
		time.Sleep(100 * time.Millisecond)
		done <- true
	}()

	select {
	case <-done:
		// Success
	case <-time.After(1 * time.Second):
		t.Error("Test timed out")
	}
}

Practical Testing Examples

Testing String Functions

package main

import (
	"strings"
	"testing"
)

func TestStringFunctions(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		contains string
		expected bool
	}{
		{"contains", "hello world", "world", true},
		{"not contains", "hello world", "xyz", false},
		{"empty", "", "test", false},
		{"case sensitive", "Hello", "hello", false},
	}

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

Testing Slices

package main

import (
	"testing"
)

func Contains(slice []int, value int) bool {
	for _, v := range slice {
		if v == value {
			return true
		}
	}
	return false
}

func TestSliceOperations(t *testing.T) {
	slice := []int{1, 2, 3, 4, 5}

	tests := []struct {
		name     string
		value    int
		expected bool
	}{
		{"found", 3, true},
		{"not found", 10, false},
		{"first", 1, true},
		{"last", 5, true},
	}

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

Testing Maps

package main

import (
	"testing"
)

func TestMapOperations(t *testing.T) {
	m := map[string]int{
		"apple":  5,
		"banana": 3,
		"cherry": 8,
	}

	tests := []struct {
		name     string
		key      string
		expected int
		exists   bool
	}{
		{"exists", "apple", 5, true},
		{"not exists", "date", 0, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			value, ok := m[tt.key]
			if ok != tt.exists {
				t.Errorf("Key %q exists = %v; want %v", tt.key, ok, tt.exists)
			}
			if ok && value != tt.expected {
				t.Errorf("m[%q] = %d; want %d", tt.key, value, tt.expected)
			}
		})
	}
}

Best Practices

✅ Good Practices

// Use table-driven tests
func TestFunction(t *testing.T) {
	tests := []struct {
		name     string
		input    interface{}
		expected interface{}
	}{
		// Test cases
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Test logic
		})
	}
}

// Use t.Helper() for helper functions
func assertEqual(t *testing.T, got, want interface{}) {
	t.Helper()
	if got != want {
		t.Errorf("got %v, want %v", got, want)
	}
}

// Test error cases
func TestError(t *testing.T) {
	_, err := functionThatErrors()
	if err == nil {
		t.Error("Expected error")
	}
}

// Use subtests for organization
t.Run("subtest", func(t *testing.T) {
	// Subtest logic
})

❌ Anti-Patterns

// Don't use generic test names
func TestFunction(t *testing.T) {
	// Hard to identify which case failed
}

// Don't ignore errors
result, _ := functionThatErrors()
// Should check error

// Don't test implementation details
// Test behavior, not internal state

// Don't create interdependent tests
// Each test should be independent

Resources

Summary

Go’s testing package provides powerful tools for writing effective tests:

  • Use table-driven tests for multiple scenarios
  • Organize tests with subtests
  • Use helper functions for common assertions
  • Test both success and error cases
  • Keep tests independent and focused
  • Aim for good coverage but prioritize meaningful tests

With these practices, you can build reliable, well-tested Go applications.

Comments

Share this article

Scan to read on mobile