Модуль: Тестирование · Уровень: Middle+/Senior
TL;DR#
testify — самая популярная вспомогательная библиотека: assert (помечает fail и продолжает тест) и require (помечает fail и немедленно останавливает через t.FailNow). Используйте require для предусловий, без которых дальнейшие проверки бессмысленны/опасны (nil-разыменование), и assert для независимых проверок, где хочется увидеть все падения сразу. Стандартной библиотеки (if got != want { t.Errorf(...) }) часто достаточно — для простых сравнений она читаема и без зависимостей; testify окупается на множестве проверок и понятных дифф-сообщениях. Для больших/структурных выходов — golden files: эталон в testdata/*.golden, обновляемый флагом -update, сравнение через go-cmp.
Теория#
assert vs require#
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUser(t *testing.T) {
u, err := repo.Get(ctx, 42)
require.NoError(t, err) // если err != nil — дальше идти нельзя, FailNow
require.NotNil(t, u) // защищаем от nil-разыменования ниже
assert.Equal(t, "alice", u.Name) // независимые проверки —
assert.Equal(t, 42, u.ID) // увидим оба падения за один прогон
assert.True(t, u.Active)
}require.*при провале вызываетt.FailNow()— останавливает текущую горутину теста. Применять, когда продолжение не имеет смысла или приведёт к панике (после получения err проверяемNoError, перед разыменованием —NotNil).assert.*при провале вызываетt.Fail()— тест помечен проваленным, но выполняется дальше. Применять для группы независимых проверок, чтобы получить полную картину за один запуск.
Важно: require нельзя вызывать из не-тестовой горутины (как и t.FailNow) — FailNow делает runtime.Goexit, завершая только свою горутину. В горутинах используйте assert + синхронизацию и проверку в основной горутине.
Полезные функции testify#
assert.Equal(t, want, got) // глубокое сравнение (ObjectsAreEqual)
assert.EqualValues(t, want, got) // с приведением типов (int32 vs int64)
assert.ElementsMatch(t, a, b) // равны как мультимножества (порядок не важен)
require.ErrorIs(t, err, ErrNotFound) // errors.Is
require.ErrorAs(t, err, &target) // errors.As
assert.ErrorContains(t, err, "denied")
assert.Eventually(t, cond, 2*time.Second, 50*time.Millisecond) // polling для асинхронного
assert.Panics(t, func(){ ... })
assert.InDelta(t, 1.0, x, 0.01) // флоаты с допуском
assert.JSONEq(t, `{"a":1}`, body) // семантическое сравнение JSONassert.Equal(t, expected, actual) — порядок аргументов важен для читаемости дифф-сообщения (expected первым).
Когда стандартной библиотеки достаточно#
func TestAdd(t *testing.T) {
if got := Add(2, 3); got != 5 {
t.Errorf("Add(2,3) = %d, want 5", got)
}
}Стандартный подход выигрывает, когда:
- сравнение простое (скаляры, строки),
- проект сознательно избегает внешних зависимостей (стандарт в Go-сообществе и в самом Go SDK — testify не используется в исходниках Go),
- нужен полный контроль над сообщением об ошибке.
Для сложных структур стандарт комбинируют с go-cmp:
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}cmp.Diff даёт читаемый дифф, поддерживает опции (cmpopts.IgnoreFields, cmpopts.EquateApprox, кастомные Equal), безопасен на неэкспортируемых полях (с опцией) — часто это лучше, чем assert.Equal для глубоких структур.
Позиция многих senior-инженеров: testify удобен и широко распространён, но добавляет зависимость и свой DSL; для библиотек предпочтительнее стандарт + go-cmp, для прикладных сервисов testify часто оправдан скоростью написания. Знание обоих и trade-off важнее догмы.
Golden files#
Эталонный вывод хранится в файле; тест сравнивает фактический вывод с ним. Подходит для больших/структурных артефактов: сгенерированный код, HTML/JSON-рендеры, форматтеры, отчёты.
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) {
got := Render(input)
golden := filepath.Join("testdata", t.Name()+".golden")
if *update {
require.NoError(t, os.WriteFile(golden, []byte(got), 0o644))
}
want, err := os.ReadFile(golden)
require.NoError(t, err)
if diff := cmp.Diff(string(want), got); diff != "" {
t.Errorf("golden mismatch (-want +got):\n%s", diff)
}
}Обновление: go test -run TestRender -update. Затем эталон ревьюится в diff и коммитится.
Достоинства: легко поддерживать большие ожидаемые выходы, изменения видны в code review как diff golden-файла. Риски: бездумный -update коммитит регрессию как «новый эталон» — golden-файлы обязаны ревьюиться человеком; нестабильный вывод (таймстемпы, мапы с рандомным порядком, указатели) делает golden флаки — нормализуйте перед сравнением (сортировка ключей, замена времени плейсхолдером). testdata — специальное имя: go test игнорирует эту директорию при сборке.
Подводные камни / gotchas#
- Путаница assert/require.
assert.NoErrorне остановит тест — следующая строка разыменует nil и упадёт паникой с непонятным стеком. Перед разыменованием —require. requireиз горутины —FailNow/Goexitзавершит только горутину, тест продолжится в неопределённом состоянии. В горутинах —assert+ сбор результатов.- Порядок аргументов
Equal(t, expected, actual)перепутан → дифф «-want +got» вводит в заблуждение. assert.Equalна функциях/каналах/несравнимых и на типах с указателями внутри даёт неинтуитивные результаты; для сложных структур надёжнееcmp.Diffс опциями.assert.Equalразличает типы:int(1) != int64(1). НуженEqualValuesили приведение.- Golden без ревью.
-updateбез вдумчивого diff закрепляет баг. Golden-файлы — часть code review. - Нестабильный golden: время, рандомизированный порядок мап, абсолютные пути, указатели. Нормализуйте вывод детерминированным.
- Зависимость ради ассертов. Для библиотеки добавление testify в
go.modнавязывает зависимость потребителям тестов — взвешивайте. assert.Error/NoErrorбез проверки конкретной ошибки маскирует «не ту» ошибку. ИспользуйтеErrorIs/ErrorAs.
Вопросы на собеседовании#
В: В чём разница между assert и require в testify?
О: assert помечает тест проваленным (t.Fail) и продолжает выполнение — годится для независимых проверок, чтобы увидеть все падения. require вызывает t.FailNow и немедленно прекращает тест — для предусловий, без которых дальше идти нельзя (после получения ошибки, перед разыменованием nil).
В: Можно ли использовать require внутри горутины?
О: Нет. require вызывает FailNow, который делает runtime.Goexit и завершает только текущую горутину, оставляя тест в неопределённом состоянии. В горутинах используют assert и собирают результаты, проверяя их в основной горутине теста.
В: Когда стандартной библиотеки достаточно и зачем тогда testify?
О: Для простых сравнений скаляров/строк if got != want { t.Errorf } читаем и без зависимостей — так пишут в самом Go SDK. testify окупается на множестве проверок, даёт лаконичный API и понятные дифф-сообщения, ErrorIs/ErrorAs/Eventually. Для глубоких структур многие предпочитают стандарт + go-cmp.Diff.
В: Что такое golden files и когда они уместны?
О: Эталонный вывод в testdata/*.golden, с которым сравнивается фактический результат; обновляется флагом -update. Уместны для больших/структурных артефактов (генерация кода, рендеры, форматтеры), где изменения удобно ревьюить как diff. Требуют детерминированного вывода и обязательного ревью при обновлении.
В: Чем cmp.Diff лучше assert.Equal для структур?
О: Даёт человекочитаемый дифф (что именно отличается), поддерживает опции (игнор полей, допуск для флоатов, кастомный Equal, обработка неэкспортируемых полей), и явно паникует на несравнимом, заставляя задать опцию. assert.Equal для глубоких структур даёт менее наглядное сообщение.
В: Почему assert.Equal(t, int32(1), int64(1)) падает?
О: testify сравнивает и значение, и тип через рефлексию; разные числовые типы не равны. Нужно assert.EqualValues (приводит типы) или явное приведение к одному типу.
В: Главный риск golden files и как его снизить?
О: Бездумный -update закрепляет регрессию как новый эталон. Снижают обязательным ревью diff golden-файлов в PR и нормализацией нестабильных частей вывода (время, порядок ключей мап, пути), чтобы тест не был флаки.
На что копают на senior+#
- Осознанный выбор: trade-off testify vs стандарт+go-cmp, отказ от зависимостей в библиотеках, единообразие в проекте.
- Семантика fail: Fail vs FailNow, поведение в горутинах, корректная обработка ошибок (
ErrorIs/Asвместо «просто была ошибка»). - Сравнение значений: go-cmp опции, кастомные Equal, обращение с неэкспортируемыми полями, временами, протобуфами, флоатами.
- Golden-дисциплина: детерминизм вывода, нормализация, ревью эталонов, защита от «обновил и закоммитил баг».
- Качество сообщений: информативность ассертов для быстрой диагностики,
t.Helper()в кастомных хелперах для правильного указания строки падения.