Пакеты, тестирование, файлы и JSON

Табличные тесты через пакет testing и go test

Содержание курса

В прошлом уроке мы вынесли функцию Square в пакет mathutil. Теперь у неё есть exported-имя и отдельная папка — всё готово, чтобы написать к ней полноценные тесты. Именно сюда и смотрит go test.

Файл _test.go, сигнатура TestXxx и выбор между t.Errorf и t.Fatalf

Создайте в папке mathutil файл mathutil_test.go. Суффикс _test.go — это не соглашение команды, а правило инструментальной цепочки Go: такие файлы компилируются только при вызове go test и не попадают в обычный бинарник пакета. Внутри объявляйте тот же пакет:

package mathutil

import "testing"

func TestSquare(t *testing.T) {
    got := Square(3)
    if got != 9 {
        t.Errorf("Square(3) = %d, want 9", got)
    }
}

Почему go test запустит TestSquare, но не запустит testSquare или squareTest? Правило discovery состоит из двух частей.

Часть 1 — имя функции. Имя должно начинаться ровно с Test, а символ сразу после Test не должен быть строчной буквой. Это означает, что подходят: TestSquare (заглавная S), Test_square (знак подчёркивания), Test (конец имени). Не подходят: Testsquare (строчная s), testSquare (сам Test написан со строчной t). Функции с нераспознанным именем go test игнорирует — они остаются обычным вспомогательным кодом.

Часть 2 — сигнатура. Если имя распознано как тест, функция обязана принимать ровно один параметр *testing.T и ничего не возвращать. Нарушение сигнатуры — это уже не тихое игнорирование: go test выдаёт ошибку сборки вида wrong signature for TestXxx. То есть неверная сигнатура при правильном имени ломает прогон, а не просто пропускается.

Теперь о выборе между двумя методами фиксации ошибок.

t.Errorf помечает тест как провальный и записывает сообщение, но выполнение функции продолжается дальше. Это полезно, когда один тест проверяет несколько независимых условий и вы хотите увидеть все нарушения сразу:

func TestSquare(t *testing.T) {
    if Square(0) != 0 {
        t.Errorf("Square(0) = %d, want 0", Square(0))
    }
    if Square(-2) != 4 {
        t.Errorf("Square(-2) = %d, want 4", Square(-2))
    }
}

Оба нарушения попадут в отчёт за один прогон.

t.Fatalf делает то же самое, но после этого немедленно останавливает текущую тест-функцию — технически через runtime.Goexit. Используйте его, когда продолжать бессмысленно: например, если первая проверка вернула nil вместо объекта, а следующий код будет разыменовывать этот указатель и вызовет панику.

result, err := ParseConfig(input)
if err != nil {
    t.Fatalf("ParseConfig вернул ошибку: %v", err)
}
// без t.Fatalf здесь был бы nil-deref
if result.Name != "expected" {
    t.Errorf("Name = %q, want \"expected\"", result.Name)
}

Практическое правило простое: если следующие строки теста зависят от того, что предыдущая проверка прошла — t.Fatalf. Если проверки независимы — t.Errorf.