Go Testing: From Zero to CI

Go has one of the best built-in testing stories of any language. No framework to install, no configuration files, no test runner debates. Just go test ./... and you are done.

This guide covers everything from writing your first test to running a full integration suite in CI.

Part 1: The Basics

Every test file ends in _test.go. Every test function starts with Test and takes a *testing.T:

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

Run it:

go test ./...

Part 2: Table-Driven Tests

The standard Go pattern for testing multiple cases:

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

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

Part 3: Integration Tests with Build Tags

Separate slow integration tests from fast unit tests using build tags:

//go:build integration

package db_test

func TestDatabaseQuery(t *testing.T) {
    // requires a running database
}

Run only unit tests: go test ./... Run everything: go test -tags=integration ./...

Part 4: CI Pipeline

Coming soon — GitHub Actions workflow, test caching, and coverage reporting.