Lesson 8 of 34 beginner 6 min read

Before this:Anatomy of a test

Your first Go test

Key takeaways Go tests need no framework: put a function named TestXxx(t *testing.T) in a file ending _test.go, and go test finds and runs it. Failures are reported through t.Errorf/t.Fatalf; a silent run means green. The loop — edit, go test, read the failure, fix — takes seconds, and this lesson walks it end-to-end, including the crucial step beginners skip: watching your test fail so you know it can.

Time to stop reading and type. This lesson builds a tiny piece of radio-flavored code and its first test, using nothing but the standard library — the same tooling GopherTrunk’s entire suite runs on. If Go isn’t installed yet, detour through the Go module’s setup lesson first.

Step 1 — the code under test

Make a directory, initialize a module, and create a function worth testing. Radio frequencies are usually stored as integer hertz and shown as megahertz, so:

mkdir freqfmt && cd freqfmt
go mod init example/freqfmt
// freq.go
package freqfmt

import "fmt"

// FormatMHz renders integer hertz as a megahertz string, e.g. 851012500 -> "851.0125 MHz".
func FormatMHz(hz int64) string {
    return fmt.Sprintf("%.4f MHz", float64(hz)/1e6)
}

Step 2 — the test file

Test code lives beside the code, in a file whose name ends _test.go. Inside, any function shaped func TestXxx(t *testing.T) is a test:

// freq_test.go
package freqfmt

import "testing"

func TestFormatMHz_TypicalFrequency(t *testing.T) {
    got := FormatMHz(851_012_500)

    want := "851.0125 MHz"
    if got != want {
        t.Errorf("FormatMHz(851012500) = %q, want %q", got, want)
    }
}

That’s a complete test: arrange (the literal input), act (one call), assert (got vs want). No framework, no imports beyond testing — the go tool itself is the runner.

Step 3 — run it

go test
PASS
ok      example/freqfmt 0.002s

Quiet output is the point: green says almost nothing, by design. Add -v to see each test named as it runs, and ./... to run every package under the current directory — the form you’ll use in real projects:

go test -v ./...

Step 4 — break it on purpose

Here’s the step that separates a real test from a decoration. Make the code wrong and watch the test catch it. Change 1e6 to 1e5 in freq.go, then:

--- FAIL: TestFormatMHz_TypicalFrequency (0.00s)
    freq_test.go:10: FormatMHz(851012500) = "8510.1250 MHz", want "851.0125 MHz"
FAIL

Read the failure like a report, because it is one: the file and line, the call and its input, what came back, what was expected. Now revert the sabotage and watch it go green again. You’ve just proven the test can fail — a test that passes no matter what the code does is worse than none, because it manufactures false confidence. This pass-fail-pass check is a habit worth keeping for every test you ever write, and in Unit 6 it becomes a formal rule: GopherTrunk doesn’t accept a bug-fix test unless it failed first against the unfixed code.

Step 5 — grow the suite at the boundaries

One test covers the happy path. The testing mindset says go hunt the edges — zero, and something enormous:

func TestFormatMHz_Zero(t *testing.T) {
    if got := FormatMHz(0); got != "0.0000 MHz" {
        t.Errorf("FormatMHz(0) = %q, want %q", got, "0.0000 MHz")
    }
}

func TestFormatMHz_GigahertzRange(t *testing.T) {
    if got := FormatMHz(1_200_000_000); got != "1200.0000 MHz" {
        t.Errorf("FormatMHz(1200000000) = %q, want %q", got, "1200.0000 MHz")
    }
}

Three tests, three named behaviors, all running in milliseconds. This is the loop working software is built in: small edit, go test, green, repeat — with any mistake surfacing seconds after you make it, while the cost curve is still at its floor.

The commands you’ll actually use

Command What it does
go test Run the current package’s tests
go test ./... Run every package below the current directory
go test -v Verbose: name each test as it runs
go test -run TestFormatMHz_Zero Run only tests matching a name pattern
go test -count=1 ./... Bypass Go’s test result cache

That -run flag is the day-to-day workhorse in a big repo: while iterating on one decoder, GopherTrunk developers run a single package — go test ./internal/scanner/ccdecoder/... — and save the full sweep for the commit gate.

Quick check: why break your code on purpose after writing a new test?

Recap

  • A Go test is a TestXxx(t *testing.T) function in a _test.go file — no framework, go test is the runner.
  • Failures speak through t.Errorf with input/got/want; a quiet run is green.
  • Break the code on purpose once per new test — pass-fail-pass proves the test can detect anything at all.
  • Grow suites toward the boundaries: zero, empty, huge.
  • go test ./..., -v, -run: the three flags that carry you through a real repo.

Next up: Table-driven tests