Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 23 из 25 ~16 мин +20 XP

Testify и стандартная библиотека: границы

Спор testify vs testing обычно начинается с неверной постановки: будто это два лагеря и надо выбрать один. На деле testify живёт поверх пакета testing и ничего из него не изымает — go test запускает те же файлы, t.Run строит те же сабтесты. Граница проходит в другом: где проверку честнее написать стандартным инструментом, а где ассерты testify окупаются.

Содержание урока · 10 разделов

Две команды в одном проекте. Первая принципиально держится стандартного тестирования go: только testing, ни одной лишней зависимости — и гордо приносит на ревью таблицу кейсов с if !reflect.DeepEqual и копипастой t.Errorf на каждый чих. Вторая поставила testify и за полгода забыла, что под ним есть пакет testing: тесты у неё запускает магия, t.Helper в хелперах не отмечен, а в бенчмарке кто-то пытался проверять результаты assert.Equal внутри цикла. Обе крайности дорого стоят. testify — надстройка над testing, а не замена: движок остаётся тем же, и граница проходит не между библиотеками, а между задачами. Ниже — карта этой границы: где stdlib сильнее, где testify окупается и почему гибрид в одном файле — норма, а не компромисс.

Testify vs testing: кто за что отвечает#

Снимем налёт магии. Файлы с тестами компилирует и запускает go test из стандартного пакета testing — testify в этом не участвует: он находит функции TestXxx(t testing.T), строит дерево сабтестов через t.Run — ровно так устроены [табличные тесты](/courses/testify/lessons/13-tablichnye-testy/) — и фильтрует их флагом -run по имени, с наклонной чертой для вложенности. t.Helper помечает функцию помощником, чтобы при провале в отчёте стояла строка места вызова, а не внутренности хелпера. Ассерты testify принимают первым аргументом TestingT — интерфейс самой testify с единственным методом Errorf, и testing.T ему послушно удовлетворяет. На этом роль testify кончается: он не умеет запускать тесты, не знает про кэш, профили и -race, отчёт собирает не он. Зато он экономит то, ради чего его берут, — строки проверок.

Бенчмарки: b.N вместо ассертов#

Первый район, где testify не нужен вообще: бенчмарки. Функция BenchmarkXxx(b testing.B) обязана гонять целевой код b.N раз, а само число b.N подбирает движок: начинает с одной итерации и увеличивает от прогона к прогону, пока замер не станет надёжным. Результат бенчмарка — наносекунды на операцию, аллокации, пропускная способность; проверять там нечего, кроме того, что код не паникует. Вставить assert технически можно — testing.B тоже имеет Errorf и формально подходит под TestingT, — но каждая проверка попадёт внутрь замера и соврёт в отчёте. Провал в бенчмарке честнее выражать b.Fatal: упал, зафиксировал, замер не состоялся. Запускается всё это не testify, а go test -bench=. с -benchmem для аллокаций — та же стандартная машина, никаких новых зависимостей.

BenchmarkParse: цикл ровно по b.N

GO
func BenchmarkParse(b *testing.B) {
	b.ResetTimer() // подготовка ниже не попадает в замер
	data := loadSample()

	for i := 0; i < b.N; i++ {
		_, err := Parse(data)
		if err != nil {
			b.Fatal(err)
		}
	}
}

Цикл обязан крутиться ровно b.N раз — число подставит движок, закладываться на конкретное значение нельзя. ResetTimer отсекает подготовку: loadSample и чтение файла не должны попадать в замер. Ошибка — b.Fatal: замер прекращается с осмысленным сообщением, никаких ассертов внутри горячего цикла. Это тестовый пакет: запускается локальным go test -bench=., песочница замеры не исполняет.

Проверка: кто выбирает b.N

Что подставит движок вместо b.N в бенчмарке?

Фаззинг: FuzzXxx и ловушка новичка#

Фаззинг живёт в стандартном тестировании go с версии 1.18: функция FuzzXxx(f testing.F), f.Add сеет стартовые входы, а f.Fuzz принимает цель вида func(t testing.T, in []byte). Дальше движок генерирует входы сам и валит цель, пока не найдёт провал. Ловушка новичка — тащить сюда привычный паттерн testify «ожидаю конкретный результат»: у фаззинга нет фиксированного want, каждый вход свой, и оракул приходится выводить из самого входа — инварианты, симметрии, отсутствие паники. Assert внутри цели компилируется: t там настоящий *testing.T. Но провал assert.Equal на сгенерированном входе — это простыня diff, по которой ничего не понять: эталона-то не было. Найденный контрпример движок сохраняет в testdata/fuzz/FuzzXxx/, и с тех пор он гоняется обычным go test как регрессия.

Фазз-цель с оракулом-инвариантом

GO
func FuzzDecode(f *testing.F) {
	f.Add([]byte("v1:42")) // сеем осмысленные входы

	f.Fuzz(func(t *testing.T, in []byte) {
		v, err := Decode(in)
		if err == nil && v.ID < 0 {
			t.Errorf("Decode вернул отрицательный ID: %d", v.ID)
		}
	})
}

Оракул — инвариант: если вход декодировался, идентификатор не может быть отрицательным. Одна строка stdlib выражает свойство точнее, чем сравнение с эталоном, которого у фаззинга нет. Сеялка f.Add даёт движку осмысленный старт, а найденный контрпример он сам спустит в тестдату — и тот станет вечным регрессионным кейсом.

Проверка: провал фазз-цели

Assert внутри фазз-цели провалился. Что сделает движок?

t.Cleanup и t.Parallel: что умеет testing сам#

Второй район — жизненный цикл теста, где testify просто не участвует. t.Cleanup регистрирует функцию, которая отработает по завершении теста и всех его сабтестов, причём в порядке LIFO: последний зарегистрированный — первый на выходе. Это удобнее defer, когда ресурс открывает функция-помощник: она же вешает очистку, а вызывающий ничего не обязан помнить. t.Parallel помечает сабтест параллельным: такие тесты приостанавливаются, дожидаются конца последовательных и возобновляются уже вместе, каждый в своей горутине. Какие гонки это порождает — тема урока про горутины и живого места ниже. testify на оба механизма не влияет: cleanup и параллельность работают в любом проекте, включая проект без единой внешней зависимости.

Что напечатает тест?

Прочитайте код и выберите, что он выведет:

func TestOrder(t *testing.T) {
	t.Cleanup(func() { fmt.Println("первый") })
	t.Cleanup(func() { fmt.Println("второй") })
	fmt.Println("тело")
}

Гибрид: t.Run, t.Cleanup и assert.Equal

GO
func TestUserRepo(t *testing.T) {
	db := openTestDB(t)
	t.Cleanup(func() { db.Close() })

	cases := []struct {
		id   int
		name string
	}{
		{1, "anna"},
		{2, "boris"},
	}
	for _, tc := range cases {
		t.Run(fmt.Sprintf("user-%d", tc.id), func(t *testing.T) {
			t.Parallel()
			u, err := db.UserByID(tc.id)
			require.NoError(t, err)
			assert.Equal(t, tc.name, u.Name)
		})
	}
}

Скелет — чистый testing: t.Run строит сабтесты, t.Cleanup закрывает базу по завершении теста целиком, вместе со всеми параллельными кейсами. Проверки — testify: require рвёт кейс при ошибке запроса, assert.Equal даёт читаемый diff по имени. Два слоя спокойно живут в одном файле. Прогон — локальный go test: песочница базу и горутины не исполняет.

Задача — инструмент#

ЗадачаИнструментПочему
Проверка значений и ошибокassert / require (testify)Читаемо, diff в отчёте, минимум копипасты
Табличные кейсы и фильтрацияt.Run и флаг -run (testing)Дерево сабтестов — зона stdlib
Замер производительностиtesting.B, b.N, -benchmemАссерты попали бы в замер и соврали
Генеративный поиск баговFuzzXxx и свой оракулФиксированного want нет — diff Equal бесполезен
Очистка ресурсовt.Cleanup / defer (testing)testify на жизненный цикл не влияет
Ускорение независимых кейсовt.Parallel (testing)Горутины и гонки — не территория testify
Моки и общий сетапmock.Mock и suite.Run (testify)Здесь библиотека реально экономит код

Где testify окупается: diff, моки, suite#

Там, где проверок много, testify платит за себя строками и отчётом. assert.Equal вместо связки if !reflect.DeepEqual плюс t.Errorf — одна строка вместо четырёх, а в отчёте при провале появляется diff: testify печатает ожидаемое и фактическое построчно, пока stdlib отдаст два слепка структуры подряд. Ошибки сравниваются ErrorsIs и ErrorAs — механика ранних уроков курса. Дальше идёт то, чего в stdlib нет вовсе: моки на mock.Mock с ожиданиями и сверкой вызовов и suite с общим SetupTest на группу тестов — это разобрано в уроке про suite. Стандартное тестирование go закрывает запуск, жизненный цикл и замеры; testify закрывает формулировку проверок и подмену зависимостей. Ссорить их — ошибка планирования, а не вкуса.

Правила гибрида#

  • go test, t.Run, t.Helper, t.Cleanup, t.Parallel — всегда stdlib: testify их не заменяет.
  • Бенчмарки — testing.B без ассертов: проверки попали бы в замер; провал — через b.Fatal.
  • Фаззингу нужен свой оракул-инвариант: фиксированного want у сгенерированного входа нет.
  • Провал фазз-цели останавливает поиск, а вход уезжает в testdata/fuzz как регрессия.
  • Проверки значений, diff в отчёте, моки и suite — территория testify.
  • Гибрид в одном файле — норма: скелет testing, формулировки testify.

Соберите бенчмарк и фазз-цель

Допишите: чьё поле N в бенчмарке, какой тип у конструктора фаззинга и какой ассерт проверит коммутативность суммы.

func BenchmarkSum(b *testing.B) { for i := 0; i < .N; i++ { _ = Sum(2, 3) } } func FuzzSum(f *testing.) { f.Add(1, 2) f.Fuzz(func(t *testing.T, a, b int) { (t, Sum(a, b), Sum(b, a), "сумма коммутативна") }) }

Соберите тест с очисткой

Расставьте строки теста: хелпер, очистка через t.Cleanup и две проверки testify.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: границы testify и testing

Термин

t.Helper

нажмите, чтобы перевернуть
Определение

Помечает функцию тестовым помощником: при провале в отчёте стоит строка места вызова, а не внутренности хелпера.

1 / 6

Граница проведена. Стандартное тестирование go отвечает за запуск, дерево сабтестов, очистку, параллельность, замеры и генеративный поиск багов; testify — за читаемость проверок, diff в отчёте, моки и suite. Спорить о замене бессмысленно, выбирать одного — тоже: в одном файле спокойно соседствуют t.Cleanup и assert.Equal, и это не компромисс, а разделение труда. Дальше по курсу — тесты в CI: там t.Parallel из этого урока встречается с -race, а вывод assert уезжает в пайплайн. Полная карта уроков — на странице testify-курса. Домашняя проверка на пять минут: найдите у себя бенчмарк или фазз-цель и убедитесь, что ассерт туда не забрался.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • testify — надстройка над testing: go test, t.Run, t.Helper остаются стандартными.
  • Бенчмарки — территория testing.B: цикл по b.N, провал через b.Fatal, ассерты в замер не тащим.
  • Фаззингу нужен свой оракул-инвариант: фиксированного want у сгенерированного входа нет.
  • Провал фазз-цели сохраняет вход в testdata/fuzz — дальше он гоняется обычным go test.
  • t.Cleanup держит LIFO-стек очистки, t.Parallel возобновляет сабтесты вместе, каждый в своей горутине.
  • Testify окупается на проверках: diff в отчёте, ErrorsIs, моки и suite.
  • Гибрид в одном файле — норма: скелет testing, формулировки testify.

Домашнее задание

Найдите в проекте тест с if !reflect.DeepEqual и перепишите его на assert.Equal, сравнив отчёты о провале до и после. Один параллельный сабтест добавьте через t.Parallel, а ресурс в хелпере закройте через t.Cleanup. Затем запустите go test -bench=. и посмотрите, сколько итераций движок подобрал сам.

Личные заметкиПусто
Хранятся только в вашем браузере (Testify · Testify и стандартная библиотека: границы)

Вопросы и ответы по уроку

Можно ли вызывать assert с *testing.B в бенчмарке?

Технически да: *testing.B имеет Errorf и удовлетворяет интерфейсу TestingT, поэтому код скомпилируется. Но каждая проверка внутри цикла попадает в замер и искажает результат, а провал через assert не останавливает бенчмарк. Идиоматично — b.Fatal или b.Error: замер прекращается честно, отчёт остаётся читаемым.

Сработает ли t.Parallel внутри suite testify?

Suite построен поверх обычных методов testing: t в методах сюиты — настоящий *testing.T, и t.Parallel формально доступен. Но Suite.Run управляет выполнением через свои хуки и параллельность методов не разруливает, так что выгода обычно иллюзорна. Параллельте сабтесты через t.Run с t.Parallel, а suite оставьте для общего сетапа.

Можно ли писать assert внутри фазз-цели?

Компилируется: цель получает настоящий *testing.T, и провал ассерта честно роняет выполнение входа. Но привычного сценария «want против got» здесь нет: у сгенерированного входа не существует эталона, а Equal на большом входе даст нечитаемую простыню. Оракул для фаззинга — инвариант или свойство; часто хватает чистой проверки средствами stdlib.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Viper25 уроков~9 чпрактика25 уроков уже доступны

Viper: конфигурация приложений на Go

Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.

  • Собирать конфиг из файлов, окружения и флагов в один реестр
  • Читать значения типизированно: Get, GetString, Unmarshal в структуры
  • Задавать приоритеты источников: флаг > env > файл > default
  • Перечитывать конфиг на лету через WatchConfig и OnConfigChange