Как работает go test
До сих пор вы проверяли программы глазами: запустили, посмотрели на вывод, сравнили с ожидаемым. Для пары строк это работает. Но когда в пакете десятки функций, после каждой правки их приходится проверять заново, и рано или поздно что-то ускользнёт. Поэтому проверку поручают тестам — коду, который вызывает ваши функции и сам сравнивает результат с ожидаемым.
Вернёмся на завод. В конце линии стоит стол контроля. Контролёр берёт готовую деталь, меряет её и записывает каждый дефект в журнал. В Go стол контроля — это команда go test, а журнал — её отчёт.
Файл с тестами
Вот функция Clamp из пакета clamp. Она должна зажимать число x в отрезок от lo до hi, но в ней ошибка:
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, через который тест сообщает о провале.
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 просит печатать отчёт подробно:
$ 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Давайте прочитаем отчёт по строкам:
=== RUN TestClamp— тест запустился.clamp_test.go:8:— файл и строка, где сработала проверка, а после двоеточия её сообщение.- Вторая такая строка пришла от строки 11: сработала и вторая проверка.
--- FAIL: TestClamp (0.00s)— итог теста и время, которое он занял.- Последняя строка — итог всего пакета:
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:
Тест 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—
С t.Fatalf в отчёте остаётся одна строка ошибки: вторая проверка в negative уже не выполняется. Но above_max всё равно запускается, потому что t.Fatalf останавливает только свой подтест. Без t.Run всё иначе: случаи идут в одном тесте, и t.Fatalf останавливает его целиком, так что до above_max дело не доходит.
Здесь картинка с контролёром перестаёт работать. На заводе деталь, снятую с линии, отправляют в брак. А t.Fatalf снимает с линии не деталь, а саму проверку: остальные функции Test в пакете всё равно запускаются.
Частая ошибка
Тестовую функцию назвали со строчной буквы:
func testClamp(t *testing.T) { // ✗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 и подпишись на канал — откроются все уроки.