GO

Go Fundamentals

18 lessons

Progress0%
1. Introduction to Go
1What is Go?
2. Variables and Data Types
1Data Types in Go
3. Control Flow
If, For, and SwitchDefer, Panic, Recover
4. Functions
Function BasicsError Handling
5. Structs and Methods
StructsMethods and Interfaces
6. Concurrency
Goroutines and ChannelsSelect and Sync
7. Maps & Slices Advanced
Slices Deep DiveMaps Operations & Patterns
8. Interfaces Deep Dive
Interface Composition & anyCommon Interfaces & Patterns
9. Packages & Modules
Package SystemGo Modules & Workspace
10. Testing & Standard Library
Testing in GoStandard Library Essentials
All Tutorials
GoTesting & Standard Library
Lesson 17 of 18 min
Chapter 10 · Lesson 1

Testing in Go

Testing in Go

Go ships with a built-in testing framework in the testing package — no third-party libraries required for basic testing.

Test Function Naming Test functions must:

  • Live in a file ending with _test.go
  • Start with Test followed by a name beginning with an uppercase letter
  • Accept a single parameter t *testing.T
go
func TestAdd(t *testing.T) { ... }

Run all tests: go test ./...

Reporting Failures

  • t.Error(args...) / t.Errorf(format, args...) — marks the test as failed but continues execution
  • t.Fatal(args...) / t.Fatalf(format, args...) — marks as failed and stops the current test immediately
  • t.Log() / t.Logf() — records output, visible only when the test fails or with -v

Table-Driven Tests The idiomatic Go testing pattern: define a slice of test cases, range over them, calling t.Run for each:

go
tests := []struct {
    name     string
    input    int
    expected int
}{ ... }
for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        got := MyFunc(tt.input)
        if got != tt.expected {
            t.Errorf("got %d, want %d", got, tt.expected)
        }
    })
}

Table-driven tests minimise boilerplate and make it easy to add cases.

Subtests with t.Run t.Run(name, func) creates a named subtest, which can be run individually:

bash
go test -run TestAdd/negative_number

Benchmark Functions Benchmark functions start with Bench and accept b *testing.B:

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

Run with go test -bench=.. The framework adjusts b.N until the benchmark runs long enough for stable measurements.

Test Coverage

bash
go test -cover ./...           # show coverage percentage
go test -coverprofile=cov.out  # save to file
go tool cover -html=cov.out    # open interactive HTML report

testing.Short() testing.Short() returns true when -short is passed. Use it to skip slow integration tests:

go
if testing.Short() {
    t.Skip("skipping slow test in short mode")
}

Example Functions Example functions document and test simultaneously. They start with Example and include an // Output: comment that the test runner verifies:

go
func ExampleAdd() {
    fmt.Println(Add(1, 2))
    // Output:
    // 3
}

Examples appear in go doc output automatically.

Code Examples

Simple unit test with testing.Tgo
package math

import (
    "testing"
)

// The function under test
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

// Basic test function
func TestDivide(t *testing.T) {
    result, err := Divide(10, 2)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if result != 5.0 {
        t.Errorf("Divide(10, 2) = %v; want 5.0", result)
    }
}

// Test the error case
func TestDivideByZero(t *testing.T) {
    _, err := Divide(10, 0)
    if err == nil {
        t.Fatal("expected error for division by zero, got nil")
    }
}

// Example function — doubles as documentation and test
func ExampleDivide() {
    result, _ := Divide(10, 4)
    fmt.Printf("%.2f\n", result)
    // Output:
    // 2.50
}

t.Fatalf stops the test immediately — use it when the test cannot continue. t.Errorf continues the test after marking it as failed. Example functions verify output automatically.

Table-driven test with t.Rungo
package stringutil

import (
    "strings"
    "testing"
)

func Palindrome(s string) bool {
    s = strings.ToLower(s)
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        if runes[i] != runes[j] {
            return false
        }
    }
    return true
}

func TestPalindrome(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  bool
    }{
        {"empty string", "", true},
        {"single char", "a", true},
        {"simple palindrome", "racecar", true},
        {"case insensitive", "RaceCar", true},
        {"not a palindrome", "hello", false},
        {"unicode palindrome", "aba", true},
        {"two chars same", "aa", true},
        {"two chars diff", "ab", false},
    }

    for _, tt := range tests {
        tt := tt // capture range variable for parallel tests
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // run subtests in parallel
            got := Palindrome(tt.input)
            if got != tt.want {
                t.Errorf("Palindrome(%q) = %v; want %v", tt.input, got, tt.want)
            }
        })
    }
}

Table-driven tests with t.Run give each case a descriptive name visible in output. t.Parallel() allows subtests to run concurrently. The tt := tt line captures the loop variable to avoid the closure trap.

Benchmark functiongo
package stringutil

import (
    "strings"
    "testing"
)

// Two implementations to benchmark
func joinConcat(parts []string) string {
    result := ""
    for _, p := range parts {
        result += p // new allocation on every iteration
    }
    return result
}

func joinBuilder(parts []string) string {
    var b strings.Builder
    b.Grow(len(parts) * 10) // pre-allocate hint
    for _, p := range parts {
        b.WriteString(p)
    }
    return b.String()
}

var parts = []string{"alpha", "beta", "gamma", "delta", "epsilon"}

func BenchmarkJoinConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        joinConcat(parts)
    }
}

func BenchmarkJoinBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        joinBuilder(parts)
    }
}

b.N is automatically tuned for a statistically reliable measurement. -benchmem shows allocations per operation. strings.Builder wins by avoiding repeated string allocations in the loop.

Quick Quiz

1. What is the difference between t.Error and t.Fatal?

2. Why do table-driven tests use `t.Run` for each case?

3. What value does `b.N` represent in a benchmark function?

4. What does an Example function's `// Output:` comment do?

Was this lesson helpful?

PreviousNext