Your First Go Test and How the go test Command Finds It
Write your first Go test and learn how go test finds it: file and function naming, test caching, -v and -run, failure output, and the vet check on test names.

Testing in Go needs no framework to install and no configuration file. The tool is go test, the library is the testing package in the standard library, and the convention is small enough to learn in ten minutes. This article writes a first test, runs it, breaks it on purpose and uses the output to explain what the command looks for and why it sometimes seems to do nothing. It is the start of the testing series, which continues with table-driven tests, subtests and parallel tests and the overview in Go testing: table tests, fuzzing and benchmarks. Everything ran on Go 1.25.6.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
The code under test
A package with two small functions:
package main
// Sum adds two integers.
func Sum(a, b int) int { return a + b }
// Abs returns the absolute value of n.
func Abs(n int) int {
if n < 0 {
return -n
}
return n
}package main
import "fmt"
func main() { fmt.Println(Sum(2, 3)) }The package is main only because the lab needs a runnable program; tests work the same in any package. In a library you would have package sum in sum.go.
The test file
package main
import "testing"
func TestSum(t *testing.T) {
got := Sum(2, 3)
if got != 5 {
t.Errorf("Sum(2, 3) = %d, want 5", got)
}
}
func TestAbs(t *testing.T) {
if got := Abs(-4); got != 4 {
t.Errorf("Abs(-4) = %d, want 4", got)
}
}
func helperNotATest(t *testing.T) { t.Fatal("never runs: the name does not start with Test") }The rules go test applies:
- File name. A file ending in
_test.gois a test file. Go ignores it in normal builds, so test code and test-only dependencies never ship in your binary. Put it next to the code, in the same directory. - Package. The file usually declares the same package as the code (
package mainhere), which gives it access to unexported identifiers. A file can instead declarepackage main_test(the package name plus_test) to test only the public API from the outside, which is the right choice for checking that your exported surface is usable. - Function name. A function is a test if it is named
Testfollowed by something that does not start with a lower-case letter, and takes one parameter,*testing.T.TestSumandTestAbsqualify.helperNotATestdoes not start withTest, so it never runs by itself; it is a helper that a test could call. - Failing. A test fails when it calls
t.Error,t.Errorf,t.Fatalort.Fatalf(or panics).Errorfmarks the test failed and continues, so you see every problem in one run.Fatalfmarks it failed and stops that test at once, which is the right choice when later lines would panic (for example, after a failed setup). A test that finishes without either has passed.
There are no assertions in the standard library, only if and t.Errorf. The convention for the message is FunctionName(inputs) = got, want expected, because anyone reading a failure needs those three things.
Running it
The run script executes the commands a developer types and prints the output, with the timings removed so the result is the same every time.
strip() { sed -E 's/[[:space:]]+[0-9.]+s$//; s/ \([0-9.]+s\)//'; }
echo '$ go test'
go test . 2>&1 | strip
echo '$ go test (again, nothing changed)'
go test . 2>&1 | strip
echo '$ go test -count=1 -v'
go test -count=1 -v . 2>&1 | strip
echo '$ go test -run TestAbs -v'
go test -count=1 -run TestAbs -v . 2>&1 | strip
echo '--- break Sum on purpose'
cp sum.go sum.go.bak
sed -i.tmp 's/return a + b/return a - b/' sum.go
echo '$ go test'
go test -count=1 . 2>&1 | strip
mv sum.go.bak sum.go; rm -f sum.go.tmp
echo '--- add a test whose name does not start with Test followed by a capital'
cp sum_test.go sum_test.go.bak
printf '\nfunc Testlowercase(t *testing.T) { t.Fatal("this would fail if it ran") }\n' >> sum_test.go
echo '$ go test'
go test -count=1 . 2>&1 | strip
mv sum_test.go.bak sum_test.go$ go test
ok example.test/lab
$ go test (again, nothing changed)
ok example.test/lab (cached)
$ go test -count=1 -v
=== RUN TestSum
--- PASS: TestSum
=== RUN TestAbs
--- PASS: TestAbs
PASS
ok example.test/lab
$ go test -run TestAbs -v
=== RUN TestAbs
--- PASS: TestAbs
PASS
ok example.test/lab
--- break Sum on purpose
$ go test
--- FAIL: TestSum
sum_test.go:8: Sum(2, 3) = -1, want 5
FAIL
FAIL example.test/lab
FAIL
--- add a test whose name does not start with Test followed by a capital
$ go test
# example.test/lab
# [example.test/lab]
./sum_test.go:20:6: Testlowercase has malformed name: first letter after 'Test' must not be lowercase
FAIL example.test/lab [build failed]
FAILgo test found the package, compiled the code and the test files into a test binary, ran it and printed ok with the package path. With no flags it prints nothing about individual tests when they pass. That is deliberate: silence means success.
The second run says (cached). go test caches successful results keyed by the test binary and its inputs (files read, environment variables consulted, command-line flags). When nothing has changed, it replays the previous result without running anything. This is why a test that depends on something outside that tracked set, such as the current time or a network service, can seem to pass forever. Run with -count=1 to disable the cache for a run, which is the standard way to force a real execution; CI should use it, or you can clear the cache with go clean -testcache.
-v prints each test as it runs: === RUN TestSum, then --- PASS: TestSum (with its duration, which the script strips). Use it when you want names, or when a test logs with t.Log, which is hidden unless the test fails or -v is on.
-run TestAbs ran only TestAbs. The argument is a regular expression matched against the test name, so -run 'Sum|Abs' runs both and -run '^TestSum$' is exact. (Running a single test or subtest covers the subtest syntax.)
What a failure looks like
The script then rewrites Sum to subtract, and runs again:
--- FAIL: TestSum
sum_test.go:8: Sum(2, 3) = -1, want 5
FAIL
FAIL example.test/lab
FAILThe line sum_test.go:8 is where t.Errorf was called, with the file and line. That is why failure messages should carry the inputs and both values: the line tells you which check, the message tells you what it saw. The exit code of go test is non-zero, so CI stops on it.
The name check
The last experiment adds func Testlowercase(t *testing.T) to the test file. You might expect Go to skip it, as it skips helperNotATest, but the run printed:
./sum_test.go:20:6: Testlowercase has malformed name: first letter after 'Test' must not be lowercase
FAIL example.test/lab [build failed]go test runs a small subset of go vet automatically before running tests, and one of those checks is on test function names. A function that looks like a test but has the wrong shape would otherwise be silently skipped, which is exactly how a test that never runs ends up in a repository for years. The same check catches wrong parameter types and mismatched names for examples and benchmarks. It is a helpful guard, and a reason to run go vet ./... in CI as well: go test only runs the cheap checks.
Where tests are discovered
go test with no arguments tests the package in the current directory. go test ./... tests every package in the module, which is the command to use in CI. go test ./internal/... limits it to a subtree, and go test example.com/mod/pkg names a package. Packages with no test files report [no test files]. A directory named testdata is ignored by the tool, which makes it the conventional home for fixture files (golden files).
A few flags worth knowing on day one
-raceturns on the race detector, which finds data races at run time. Use it in CI for any code with goroutines.-coverreports statement coverage;-coverprofile=c.outwrites a profile forgo tool cover -html=c.out.-failfaststops at the first failing test.-timeout 30schanges the default 10 minute limit for the whole test binary.-shuffle=onruns tests in random order, which finds hidden dependencies between tests.-shortsetstesting.Short()to true so slow tests can skip themselves.
Habits to start with
Write the test file at the same time as the code. Name tests after the thing tested (TestParseDate, not TestFunction1). Keep each test small enough that its failure message identifies the problem. Run the whole suite before you commit. When the test grows beyond a few checks of the same shape, move to a table (table-driven tests). The command reference is in the go test documentation and the testing package.


