Skip to content
gohacks.dev
Testing

Test Helpers in Go and Why t Helper Matters for Failures

Without t.Helper a failing check inside a helper is reported at the helper's line. See it fail, fix it, and learn why every helper layer needs the call.

4 min read
Summarize in AI
Flat illustration of a test report with one failing test.

A test helper is a function that does a check on behalf of a test: compare two values, load a fixture, start a server. Helpers remove repetition and make tests read like specifications. They have one failure mode that makes the output worse than the repetition they replaced: when a check inside a helper fails, the failure message points at the line inside the helper, and that line is the same for every caller.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

t.Helper() fixes it. This tip shows the problem and the fix with real output, and the case that catches people: helpers that call helpers. It goes with your first test and the failure messages in table-driven tests.

The problem and the fix

Two helpers do the same comparison. One calls t.Helper(), the other does not.

helper_test.goGo
package main

import "testing"

// Without t.Helper, a failure is reported at the line inside this function, which is the same for every caller.
func assertEqualBad(t *testing.T, got, want int) {
	if got != want {
		t.Errorf("got %d, want %d", got, want)
	}
}

// With t.Helper, the failure is reported where the helper was called, which is the line you need to look at.
func assertEqualGood(t *testing.T, got, want int) {
	t.Helper()
	if got != want {
		t.Errorf("got %d, want %d", got, want)
	}
}

func TestWithoutHelper(t *testing.T) {
	assertEqualBad(t, 1+1, 3)
	assertEqualBad(t, 2*2, 5)
}

func TestWithHelper(t *testing.T) {
	assertEqualGood(t, 1+1, 3)
	assertEqualGood(t, 2*2, 5)
}

// Helpers that call helpers: every layer needs t.Helper, or the report points at the layer that forgot.
func mustPositive(t *testing.T, n int) {
	t.Helper()
	requireTrue(t, n > 0, "expected a positive number")
}

func requireTrue(t *testing.T, ok bool, msg string) {
	if !ok {
		t.Fatal(msg)
	}
}

func TestNestedHelper(t *testing.T) {
	mustPositive(t, -4)
}
main.goGo
package main

func main() {}
Shell
for name in TestWithoutHelper TestWithHelper TestNestedHelper; do
  echo "\$ go test -run '^$name\$'"
  go test -count=1 -run "^$name\$" . 2>&1 | grep -E '^(--- FAIL|    helper_test)' | sed -E 's/ \([0-9.]+s\)//'
done
Text
$ go test -run '^TestWithoutHelper$'
--- FAIL: TestWithoutHelper
    helper_test.go:8: got 2, want 3
    helper_test.go:8: got 4, want 5
$ go test -run '^TestWithHelper$'
--- FAIL: TestWithHelper
    helper_test.go:26: got 2, want 3
    helper_test.go:27: got 4, want 5
$ go test -run '^TestNestedHelper$'
--- FAIL: TestNestedHelper
    helper_test.go:38: expected a positive number

Each test calls its helper twice with a wrong answer.

Without t.Helper():

Text
helper_test.go:8: got 2, want 3
helper_test.go:8: got 4, want 5

Both failures are reported at line 8, the t.Errorf inside the helper. They look as if the same check failed twice. In a test with twenty assertions, you cannot tell which ones failed without reading the values and searching for them.

With t.Helper():

Text
helper_test.go:26: got 2, want 3
helper_test.go:27: got 4, want 5

Now each failure is attributed to the line in the test where the helper was called. Click the line in your editor and you are at the failing check.

What t.Helper does

t.Helper() marks the calling function as a helper. When the testing package builds the file and line for a log message, it walks up the call stack and skips frames that are marked as helpers, stopping at the first one that is not. So the reported location becomes the code that called the helper. It costs nothing worth worrying about, and it should be the first line of the function, before anything that might log or fail.

It applies to t.Log, t.Error, t.Fatal and their formatted variants, to t.Skip, and to *testing.B and *testing.F the same way.

The layered case

The third test is the one that bites. mustPositive calls t.Helper() and delegates the check to requireTrue, which does not. The test calls mustPositive(t, -4) on line 43. The report:

Text
helper_test.go:38: expected a positive number

Line 38 is the t.Fatal inside requireTrue. The stack walk skipped mustPositive (a helper) and stopped at requireTrue (not marked), and reported there. The caller's line 43 never appears. The rule: every function in the chain between the test and the failing call needs t.Helper(). If a helper delegates to another function that can fail the test, that function needs it too.

Habits

  • Make t.Helper() the first line of any function that takes a *testing.T and fails it for its caller. It is cheap and wrong to omit.
  • Do not mark the test function itself, or functions that are the meaningful place for the failure to be reported (a function whose own line is informative).
  • Prefer helpers that take t and fail over helpers that return an error for the test to check, when the check is always the same: the call site stays one line.
  • Use t.Cleanup in helpers that create resources (a temp file, a server), so callers do not need a matching teardown line (cleanup).
  • Name helpers for what they assert or build: mustParse, assertJSONEqual, newTestServer.
  • Consider t.Fatal versus t.Error deliberately: a setup helper that cannot continue should call Fatal; a comparison helper should usually call Error so the test shows every difference.

The helper machinery is described in the testing package documentation for T.Helper.

Also inBasics
Taggedquick tip
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.