Skip to content
Theory · From a file to a project · lesson 5 of 10

How go test works

Until now you checked programs by eye: run it, look at the output, compare it with what you expected. For a couple of lines that works. But once a package has dozens of functions, every change means checking them all again, and sooner or later something slips through. So the checking is handed to tests — code that calls your functions and compares the result with the expected one by itself.

Back to the factory. At the end of the line stands the inspection desk. The inspector takes a finished part, measures it and writes every defect into a log. In Go the inspection desk is the go test command, and the log is its report.

A test file

Here is the Clamp function of the clamp package. It should clamp the number x into the range from lo to hi, but it has a bug:

GO
package clamp

func Clamp(x, lo, hi int) int {
    if x > hi {
        return hi
    }
    return x
}

The tests of a package go next to it, in the same directory, in a file whose name ends in _test.go. These are the rules go test uses to find tests:

  • the file name ends in _test.go, for example clamp_test.go;
  • the file declares the same package, package clamp, and imports the testing package;
  • a test function is named TestXxx: Test is followed by a capital letter;
  • it takes one parameter, t *testing.T, through which the test reports a failure.
GO
package clamp

import "testing"

func TestClamp(t *testing.T) {
    got := Clamp(-3, 0, 10)
    if got != 0 {
        t.Errorf("Clamp(-3, 0, 10) = %d, want 0", got)
    }
    if got < 0 {
        t.Errorf("%d is below lo", got)
    }
}

The test calls Clamp(-3, 0, 10) and makes two checks. The first compares the result with the expected 0. The second checks that the result is not below lo. When a check fails, t.Errorf writes a message into the report.

How go test runs tests

go test builds the package together with its _test.go files into one test program and runs every Test function in it, one after another. The _test.go files never get into a normal build: go build does not see them. The -v flag asks for a detailed report:

TEXT
$ go test -v
=== RUN   TestClamp
    clamp_test.go:8: Clamp(-3, 0, 10) = -3, want 0
    clamp_test.go:11: -3 is below lo
--- FAIL: TestClamp (0.00s)
FAIL
exit status 1
FAIL	example.com/clamp	0.007s

Let's read the report line by line:

  1. === RUN TestClamp — the test has started.
  2. clamp_test.go:8: — the file and line where a check fired, and after the colon its message.
  3. The second such line came from line 11: the second check fired too.
  4. --- FAIL: TestClamp (0.00s) — the test's result and the time it took.
  5. The last line is the result of the whole package: FAIL, the package path and the time. Had everything passed, it would say ok.

t.Errorf and t.Fatalf

There are two ways to report a failure, and the difference between them is a common interview question:

  • t.Errorf marks the test as failed and goes on: the following checks run too;
  • t.Fatalf marks the test as failed and stops it at once: the lines after the call do not run.

An inspector with t.Errorf writes the defect down and keeps measuring the part. An inspector with t.Fatalf writes the defect down and takes the part off the line. That is why the report above has two lines: both checks use t.Errorf.

In the widget the same Clamp is checked with a table of three cases, and each case runs through t.Run as a separate subtest, such as TestClamp/negative. Run the test, switch the first check to t.Fatalf, and watch how many error lines remain and whether above_max runs:

Livego test: Errorf and Fatalfnot run yet
Test clamp_test.go:1–4package clamp · import "testing"5func TestClamp(t *testing.T) {6	cases := []struct {7		name            string8		x, lo, hi, want int9	}{10		{"inside", 5, 0, 10, 5},11		{"negative", -3, 0, 10, 0},12		{"above_max", 15, 0, 10, 10},13	}14	for _, c := range cases {15		t.Run(c.name, func(t *testing.T) {16			got := Clamp(c.x, c.lo, c.hi)17			if got != c.want {18				t.Errorf("got %d, want %d", got, c.want)19			}20			if got < c.lo {21				t.Errorf("%d is below lo", got)22			}23		})24	}25}
  • inside—
  • negative—
  • above_max—
$ go test -v
the go test -v output appears here

One press runs all 3 cases

With t.Fatalf one error line remains: the second check in negative no longer runs. But above_max still runs, because t.Fatalf stops only its own subtest. Without t.Run it is different: the cases run in one test, and t.Fatalf stops all of it, so above_max is never reached.

This is where the inspector picture stops working. In a factory, a part taken off the line goes to scrap. t.Fatalf takes off the line not the part but the check itself: the other Test functions in the package still run.

Common mistake

The test function was named with a lowercase letter:

GO
func testClamp(t *testing.T) { // ✗
TEXT
testing: warning: no tests to run
PASS
ok  	example.com/clamp	0.006s

The code compiles, but go test looks only for functions that start with Test, and it does not run testClamp. The report says PASS, although not a single check ran. So read the no tests to run warning, not only the result.

Key points

  • Tests live in _test.go files; a test function is named TestXxx and takes t *testing.T.
  • go test -v prints === RUN, error lines of the form file:line: message, and a --- PASS or --- FAIL result.
  • t.Errorf marks a failure and goes on; t.Fatalf marks a failure and stops the test.
  • Inside t.Run, t.Fatalf stops only its own subtest; the other cases still run.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way