# Testing

> Source: https://learn-go.org/testing/
> Part of Learn Go, free to read.

Go's test framework is part of the standard library and the toolchain. No dependency, no configuration.

```go pricing.go
package pricing

func Discount(total, threshold int, pct float64) int {
    if total < threshold {
        return total
    }
    return total - int(float64(total)*pct)
}
```

```go pricing_test.go
package pricing

import "testing"

func TestDiscountBelowThreshold(t *testing.T) {
    got := Discount(500, 1000, 0.1)
    if got != 500 {
        t.Errorf("Discount(500, 1000, 0.1) = %d, want 500", got)
    }
}
```

```bash
go test ./...
go test -v ./pricing          # show each test
go test -run TestDiscount ./pricing
```

The rules: the file ends in `_test.go`, the function starts with `Test`, and it takes `*testing.T`.

`t.Errorf` records a failure and continues; `t.Fatalf` stops the test immediately. Use `Fatalf` when continuing would panic — a nil result you are about to dereference.

## Error messages that help

Go has no assertion library by design, so the message you write *is* the diagnostic. The convention:

```go
t.Errorf("Discount(%d, %d, %.2f) = %d, want %d", total, threshold, pct, got, want)
```

Input, actual, expected. A failure should tell you what happened without opening the test.

## Table-driven tests

The dominant Go testing idiom. One test function, many cases:

```go
func TestDiscount(t *testing.T) {
    tests := []struct {
        name      string
        total     int
        threshold int
        pct       float64
        want      int
    }{
        {"below threshold", 500, 1000, 0.1, 500},
        {"at threshold", 1000, 1000, 0.1, 900},
        {"above threshold", 2000, 1000, 0.1, 1800},
        {"zero percent", 2000, 1000, 0, 2000},
        {"zero total", 0, 1000, 0.1, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Discount(tt.total, tt.threshold, tt.pct)
            if got != tt.want {
                t.Errorf("Discount(%d, %d, %.2f) = %d, want %d",
                    tt.total, tt.threshold, tt.pct, got, tt.want)
            }
        })
    }
}
```

`t.Run` creates a subtest, so a failure names the case:

```text
--- FAIL: TestDiscount/at_threshold
```

Adding a case is one line. That is what makes this style stick — it lowers the cost of testing the boundary conditions people otherwise skip.

## Helpers

```go
func mustUser(t *testing.T, id string) *User {
    t.Helper()                  // failures report the CALLER's line
    u, err := LoadUser(id)
    if err != nil {
        t.Fatalf("LoadUser(%q): %v", id, err)
    }
    return u
}
```

`t.Helper()` is the important line — without it every failure points at the helper instead of the test that called it.

## Setup and cleanup

```go
func TestWithTempDir(t *testing.T) {
    dir := t.TempDir()       // created, and removed automatically
    f := filepath.Join(dir, "data.json")

    srv := startServer(t)
    t.Cleanup(func() { srv.Close() })    // runs at the end, even on failure
    // ...
}
```

`t.TempDir()` and `t.Cleanup()` remove most of the bookkeeping. `t.Context()` gives you a context cancelled when the test finishes.

## Testing HTTP

`httptest` covers both directions — a fake server for your client, and a fake request for your handler.

```go
func TestHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/users/1", nil)
    rec := httptest.NewRecorder()

    Handler(rec, req)

    if rec.Code != http.StatusOK {
        t.Fatalf("status = %d, want 200", rec.Code)
    }
    if !strings.Contains(rec.Body.String(), "ada") {
        t.Errorf("body = %q, missing user", rec.Body.String())
    }
}

func TestClientRetries(t *testing.T) {
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusTooManyRequests)
    }))
    defer srv.Close()

    _, err := NewClient(srv.URL).Get(t.Context(), "/x")
    if err == nil {
        t.Fatal("expected an error after retries are exhausted")
    }
}
```

## Comparing structs

`==` works for comparable types; for anything with slices or maps use `go-cmp`:

```go
if diff := cmp.Diff(want, got); diff != "" {
    t.Errorf("mismatch (-want +got):\n%s", diff)
}
```

The diff output is the reason — it shows the one field that differs rather than dumping two large structs.

## The flags that matter

```bash
go test -race ./...           # data race detector — always
go test -count=1 ./...        # disable the result cache
go test -cover ./...
go test -coverprofile=c.out ./... && go tool cover -html=c.out
go test -run TestX -v ./pkg
go test -short ./...          # skip long tests via testing.Short()
```

**`-race` is not optional** for anything concurrent. It roughly doubles runtime and finds a class of bug that is otherwise found in production, non-reproducibly. `-count=1` matters when iterating, because a cached pass is not a pass.

## Benchmarks

```go
func BenchmarkDiscount(b *testing.B) {
    b.ReportAllocs()
    for b.Loop() {
        Discount(2000, 1000, 0.1)
    }
}
```

```bash
go test -bench=. -benchmem ./...
go test -bench=. -count=10 ./... > new.txt && benchstat old.txt new.txt
```

`-benchmem` reports allocations per operation, which is a steadier signal than nanoseconds. `benchstat` tells you whether a difference is real or noise — a single run is not evidence.

## Fuzzing

```go
func FuzzParse(f *testing.F) {
    f.Add("key=value")                   // seed corpus
    f.Fuzz(func(t *testing.T, input string) {
        _, _ = Parse(input)              // must not panic on any input
    })
}
```

```bash
go test -fuzz=FuzzParse -fuzztime=60s ./pkg
```

Worth writing for anything that parses input you do not control.

## Exercise

```go
// Given: func Slugify(s string) string
// Write a table-driven test covering:
//   normal text, text with punctuation, leading/trailing spaces,
//   an empty string, and a non-ASCII string.
// Use t.Run subtests and a helpful error message format.
```

## Common questions

### Should I use testify or another assertion library?

You do not need one. Table-driven tests plus `cmp.Diff` cover almost everything, with one fewer dependency and no assertion DSL for readers to learn. Testify is widely used and fine if your team prefers it — just do not mix both styles.

### What coverage percentage should I target?

None. A target gets satisfied by tests that execute code without asserting anything useful, which is worse than no test because it looks like coverage. Use the coverage report to find untested paths that matter, then judge which are worth testing.

### Internal or external test package?

`package pricing` gives access to unexported identifiers; `package pricing_test` forces you to use the public API, which is a better test of the design. Use the internal form for genuinely internal logic and the external form for the package's contract.
