Перейти к содержимому
Теория · От файла к проекту · урок 5 из 10

Как работает go test

До сих пор вы проверяли программы глазами: запустили, посмотрели на вывод, сравнили с ожидаемым. Для пары строк это работает. Но когда в пакете десятки функций, после каждой правки их приходится проверять заново, и рано или поздно что-то ускользнёт. Поэтому проверку поручают тестам — коду, который вызывает ваши функции и сам сравнивает результат с ожидаемым.

Вернёмся на завод. В конце линии стоит стол контроля. Контролёр берёт готовую деталь, меряет её и записывает каждый дефект в журнал. В Go стол контроля — это команда go test, а журнал — её отчёт.

Файл с тестами

Вот функция Clamp из пакета clamp. Она должна зажимать число x в отрезок от lo до hi, но в ней ошибка:

GO
package clamp

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

Тесты к пакету кладут рядом, в тот же каталог, в файл с именем на _test.go. Вот правила, по которым go test находит тесты:

  • имя файла заканчивается на _test.go, например clamp_test.go;
  • файл объявляет тот же пакет, package clamp, и импортирует пакет testing;
  • тестовая функция называется TestXxx: после Test идёт заглавная буква;
  • она принимает один параметр t *testing.T, через который тест сообщает о провале.
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)
    }
}

Тест вызывает Clamp(-3, 0, 10) и делает две проверки. Первая сравнивает результат с ожидаемым 0. Вторая проверяет, что результат не меньше lo. Если проверка не прошла, t.Errorf записывает сообщение в отчёт.

Как go test запускает тесты

Команда go test собирает пакет вместе с файлами _test.go в одну тестовую программу и запускает в ней каждую функцию Test по очереди. Файлы _test.go в обычную сборку не попадают: go build их не видит. Флаг -v просит печатать отчёт подробно:

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

Давайте прочитаем отчёт по строкам:

  1. === RUN TestClamp — тест запустился.
  2. clamp_test.go:8: — файл и строка, где сработала проверка, а после двоеточия её сообщение.
  3. Вторая такая строка пришла от строки 11: сработала и вторая проверка.
  4. --- FAIL: TestClamp (0.00s) — итог теста и время, которое он занял.
  5. Последняя строка — итог всего пакета: FAIL, путь пакета и время. Если бы всё прошло, там стояло бы ok.

t.Errorf и t.Fatalf

Сообщить о провале можно двумя способами, и разница между ними — частый вопрос на собеседовании:

  • t.Errorf отмечает тест проваленным и идёт дальше: следующие проверки тоже выполняются;
  • t.Fatalf отмечает тест проваленным и сразу останавливает его: строки после вызова не выполняются.

Контролёр с t.Errorf записывает дефект и меряет деталь дальше. Контролёр с t.Fatalf записывает дефект и снимает деталь с линии. Поэтому в отчёте выше две строки: обе проверки написаны через t.Errorf.

В виджете тот же Clamp проверяют таблицей из трёх случаев, и каждый случай запускается через t.Run как отдельный подтест, например TestClamp/negative. Запустите тест, переключите первую проверку на t.Fatalf и посмотрите, сколько строк ошибок останется и запустится ли above_max:

Вживуюgo test: Errorf и Fatalfещё не запускали
Тест 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
вывод go test -v появится здесь

Одно нажатие — все 3 случая

С t.Fatalf в отчёте остаётся одна строка ошибки: вторая проверка в negative уже не выполняется. Но above_max всё равно запускается, потому что t.Fatalf останавливает только свой подтест. Без t.Run всё иначе: случаи идут в одном тесте, и t.Fatalf останавливает его целиком, так что до above_max дело не доходит.

Здесь картинка с контролёром перестаёт работать. На заводе деталь, снятую с линии, отправляют в брак. А t.Fatalf снимает с линии не деталь, а саму проверку: остальные функции Test в пакете всё равно запускаются.

Частая ошибка

Тестовую функцию назвали со строчной буквы:

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

Код компилируется, но go test ищет только функции, которые начинаются с Test, и testClamp не запускает. Отчёт говорит PASS, хотя ни одна проверка не выполнялась. Поэтому читайте предупреждение no tests to run, а не только итог.

Главное

  • Тесты лежат в файлах _test.go, а тестовая функция называется TestXxx и принимает t *testing.T.
  • go test -v печатает === RUN, строки ошибок вида файл:строка: сообщение и итог --- PASS или --- FAIL.
  • t.Errorf отмечает провал и идёт дальше, t.Fatalf отмечает провал и останавливает тест.
  • Внутри t.Run вызов t.Fatalf останавливает только свой подтест, остальные случаи запускаются.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом