Writing Your First Test in Go

How Go tests work with the built-in testing package, _test.go files, Test functions, t.Errorf vs t.Fatalf, and reading a failing test.

Go has testing built in. There is no framework to install and no special runner to configure. You write functions in files ending in _test.go, and go test finds and runs them. This page covers the rules for writing a test and how to read the result.

The Code to Test

Start with a small function in a package called textutil:

// file: textutil/slug.go
package textutil

import "strings"

// Slug turns a title into a URL-friendly slug.
func Slug(title string) string {
	return strings.ReplaceAll(strings.ToLower(strings.TrimSpace(title)), " ", "-")
}

The Test File

Tests live next to the code, in a file whose name ends in _test.go:

// file: textutil/slug_test.go
package textutil

import (
	"strings"
	"testing"
)

func Slug(title string) string {
	return strings.ReplaceAll(strings.ToLower(strings.TrimSpace(title)), " ", "-")
}

func TestSlug(t *testing.T) {
	got := Slug("  Hello World ")
	want := "hello-world"
	if got != want {
		t.Errorf("Slug(%q) = %q, want %q", "  Hello World ", got, want)
	}
}

The Slug function is repeated in this file only so the example runs on its own. In a real project it stays in slug.go, and the test file contains just the test.

The rules go test uses to find tests:

  • the file name ends in _test.go
  • the function name starts with Test followed by a capital letter or nothing else, like TestSlug
  • it takes exactly one parameter, t *testing.T

Run it from the package folder:

go test
PASS
ok  	example.com/site/textutil	0.002s

_test.go files are only compiled by go test. They are never part of your normal build.

Reading a Failing Test

Change want to "hello_world" and run again:

--- FAIL: TestSlug (0.00s)
    slug_test.go:15: Slug("  Hello World ") = "hello-world", want "hello_world"
FAIL
exit status 1
FAIL	example.com/site/textutil	0.002s

The message you passed to t.Errorf is printed with its file and line. A good failure message names the input, what you got and what you wanted, so you can understand the failure without opening the test.

Go has no assert functions:

The standard library has no assertEqual. You write a normal if and call t.Errorf. It feels verbose at first, but messages are exactly as clear as you make them. Libraries such as testify add assertions if your team prefers them.

t.Errorf vs t.Fatalf

Both report a failure. The difference is what happens next:

  • t.Errorf marks the test as failed and keeps going, so you can see several problems in one run.
  • t.Fatalf marks it as failed and stops this test immediately.

Use Fatalf when continuing makes no sense, usually because a later line would crash:

// file: config_test.go
package config

import (
	"encoding/json"
	"testing"
)

type Config struct {
	Port int    `json:"port"`
	Mode string `json:"mode"`
}

func TestParseConfig(t *testing.T) {
	var c Config
	err := json.Unmarshal([]byte(`{"port": 8080, "mode": "dev"}`), &c)
	if err != nil {
		t.Fatalf("unmarshal failed: %v", err) // nothing below can work
	}
	if c.Port != 8080 {
		t.Errorf("Port = %d, want 8080", c.Port)
	}
	if c.Mode != "dev" {
		t.Errorf("Mode = %q, want %q", c.Mode, "dev")
	}
}

If parsing fails, checking the fields is pointless, so Fatalf stops. If parsing works, both field checks run, and both mismatches would be reported together.

Fatal from another goroutine:

t.Fatal and t.FailNow must be called from the goroutine running the test. Calling them inside a goroutine you started does not stop the test correctly, and go vet reports call to (*T).Fatalf from a non-test goroutine. Send the error back over a channel or use t.Errorf there.

Same Package or _test Package

A test file can use the same package name as the code, as above, which lets tests call unexported functions. It can also use package textutil_test, which only sees exported names, the same as a real user of the package:

package textutil_test

import (
	"testing"

	"example.com/site/textutil"
)

func TestSlugPublic(t *testing.T) {
	if got := textutil.Slug("Go Docs"); got != "go-docs" {
		t.Errorf("got %q", got)
	}
}

Both styles are common. The _test package is a good check that your public API is usable on its own.

Your first test works:

If go test prints ok with your package path, and a deliberately broken expectation prints a clear --- FAIL with input, got and want, your test setup is complete.

Summary

Go tests are functions named TestXxx(t *testing.T) in _test.go files next to the code, run with go test. Report problems with t.Errorf to keep going or t.Fatalf to stop, and write messages that show the input, the result and the expected value. Test files can share the package to reach unexported code, or use a _test package to test only the public API.