Irbisa · Шпаргалка

Тестирование и бенчмарки

Табличные тесты, httptest, бенчмарки, фаззинг, -race в CI

Middle-разработчик · 25 вопросов

  1. 01

    Как go test находит и запускает тесты и что делают ./..., -run, -v и -count=1?

    go test собирает пакет вместе с его файлами _test.go в тестовый бинарник и запускает в нём каждую функцию func TestXxx(t *testing.T); файлы и функции с другими именами игнорируются.

  2. 02

    Что такое табличный тест в Go и что добавляет обёртка каждого кейса в t.Run по сравнению с обычным циклом for?

    Табличный тест описывает кейсы срезом структур (имя, вход, ожидаемый результат) и прогоняет по каждому одну и ту же проверку; t.Run превращает каждый кейс в подтест со своим именем и своим *testing.T.

    func TestParseDuration(t *testing.T) {	cases := []struct {		name    string		in      string		want    time.Duration		wantErr bool…
  3. 03

    Чем отличаются t.Error и t.Fatal и когда использовать каждый?

    t.Error помечает тест упавшим и даёт ему продолжить работу; t.Fatal помечает его упавшим и сразу останавливает: после этой строки ничего не выполняется.

  4. 04

    Зачем тестовый хелпер начинается с t.Helper() и почему его первый параметр обычно testing.TB, а не *testing.T?

    t.Helper() заставляет падение внутри хелпера указывать на строку, где хелпер вызвали, а не на его собственный t.Fatalf; интерфейс testing.TB реализуют *testing.T, *testing.B и *testing.F, поэтому один хелпер работает и в тестах, и в бенчмарках, и в фазз-тестах.

    // testing.TB, not *testing.T: the same fixture works in a benchmark.func mustStore(tb testing.TB, seed ...Row) *Store {	tb.Helper() // failures below are reported at the CALLER's line 	s, err := Open(tb.TempDir())	if err != nil {…
  5. 05

    Почему некоторые тестовые файлы объявляют package foo_test, а не package foo, и чем за это приходится платить?

    package foo_test объявляет внешний тестовый пакет: он лежит в том же каталоге и попадает в тот же тестовый бинарник, но компилируется отдельно, поэтому видит только экспортированный API foo и может импортировать пакеты, которые сами импортируют foo.

    // store/export_test.go: package store, compiled only under `go test`package store // Give the external tests a controlled view of the internals.var ParseKey = parseKey…
  6. 06

    Что такое функция Example с комментарием // Output: и как go test её проверяет?

    Функция ExampleXxx() в файле _test.go служит исполняемой документацией: go test запускает её, перехватывает стандартный вывод и сравнивает с комментарием // Output:, а pkg.go.dev показывает её рядом с Xxx.

    package money_test import (	"fmt" 	"example.com/money"…
  7. 07

    Что умеет t.Cleanup такого, чего не может defer внутри того же хелпера, и когда defer всё-таки лучше?

    t.Cleanup(f) привязывает уборку к тесту, а не к текущей функции, поэтому хелпер сам освобождает то, что открыл, вместо того чтобы отдавать вызывающему func(), о котором надо помнить.

    // wrong: the caller has to remember the teardown, and some will notfunc openDBWithCleanup(t *testing.T) (*sql.DB, func()) {	db := dial(t)	return db, func() { db.Close() }}…
  8. 08

    Что такое TestMain и когда он нужен вместо подготовки в каждом тесте?

    func TestMain(m *testing.M) заменяет сгенерированный main тестового бинарника пакета: он выполняется один раз на пакет, делает подготовку, вызывает m.Run(), чтобы прогнать все тесты, и затем убирает за собой.

    var testDB *sql.DB func TestMain(m *testing.M) {	flag.Parse() // needed before testing.Short() or custom flags	if testing.Short() {		m.Run()…
  9. 09

    Этот тест зависает до 10-минутного -timeout каждый раз, когда fetch возвращает ошибку. Почему и как горутина должна сообщать об ошибке?

    func TestFetch(t *testing.T) {	var wg sync.WaitGroup	wg.Add(1)	go func() {		resp, err := fetch(context.Background(), "/health")		if err != nil {			t.Fatalf("fetch: %v", err)		}		check(t, resp)		wg.Done()	}()	wg.Wait()}

    t.Fatalf останавливает только ту горутину, которая его вызвала (здесь воркер, а не тест), потому что выходит через runtime.Goexit.

  10. 10

    При go 1.21 в go.mod каждый параллельный подтест ниже проверяет последний кейс; после смены строки на go 1.22 всё работает. Почему?

    for _, tc := range cases {	t.Run(tc.name, func(t *testing.T) {		t.Parallel()		if err := Validate(tc.in); err == nil {			t.Errorf("Validate(%+v) = nil, want error", tc.in)		}	})}

    До Go 1.22 у цикла range была одна переменная tc, которой присваивалось новое значение на каждой итерации.

  11. 11

    Найдите баг: этот табличный тест остался зелёным после изменения, из-за которого ParsePort стал отвергать любой порт.

    func TestParsePort(t *testing.T) {	cases := []struct {		in      string		want    int		wantErr bool	}{{"8080", 8080, false}, {"70000", 0, true}, {"http", 0, true}}	for _, tc := range cases {		t.Run(tc.in, func(t *testing.T) {			got, err := ParsePort(tc.in)			if err != nil {				return // the error cases			}			if got != tc.want {				t.Errorf("ParsePort(%q) = %d, want %d", tc.in, got, tc.want)			}		})	}}

    Подтест выходит при любой ошибке, не глядя на tc.wantErr, поэтому неожиданная ошибка делает валидный кейс зелёным.

  12. 12

    Когда тестировать через httptest.NewRecorder, а когда через httptest.NewServer, и чего рекордер не воспроизводит?

    Рекордер тестирует хендлер, а тестовый сервер клиента: httptest.NewRecorder создаёт фейковый http.ResponseWriter, который передаётся прямо в ServeHTTP, а httptest.NewServer поднимает настоящий HTTP-сервер на loopback-порту.

    // Handler under test: no socket, no client, no transport.func TestCreateOrder(t *testing.T) {	req := httptest.NewRequest(http.MethodPost, "/orders",		strings.NewReader(`{"sku":"A1","qty":2}`))	rec := httptest.NewRecorder()…
  13. 13

    Как покрыть юнит-тестами сервис, который ходит в базу и во внешний API, не поднимая ни то ни другое, и писать фейки руками или генерировать моки?

    Сервис должен зависеть от небольших интерфейсов, объявленных там, где их используют, а в тесте ему передаётся двойник: чаще всего фейк, написанный руками, а сгенерированный мок (gomock, mockery) нужен, только когда сами вызовы и есть проверяемое поведение.

    // Declared in the service package: only what the service calls.type UserRepo interface {	User(ctx context.Context, id int64) (User, error)	Save(ctx context.Context, u User) error}…
  14. 14

    Тест на истечение токена спит две секунды и всё равно падает на CI примерно раз в неделю. Как проверить истечение, не завися от реальных часов?

    Внедрите часы: код получает «сейчас» из зависимости (поля func() time.Time или интерфейса с одним методом), прод передаёт time.Now, а тест подставляет фейк, который сам двигает вперёд.

    type Session struct {	expires time.Time	now     func() time.Time // the seam} func NewSession(ttl time.Duration, now func() time.Time) *Session {…
  15. 15

    В тесте рендерера зашита ожидаемая строка на 200 строк, которая ломается при каждой правке шаблона. Как перейти на golden-файлы и не получить тест, который никогда не падает?

    Храните ожидаемый вывод в testdata/ и перезаписывайте его только под своим флагом -update: обычный прогон сравнивает с закоммиченным файлом, а перегенерация видна на код-ревью как дифф.

    var update = flag.Bool("update", false, "rewrite testdata golden files") func TestRenderInvoice(t *testing.T) {	for _, name := range []string{"single_line", "multi_currency", "zero_total"} {		t.Run(name, func(t *testing.T) {			got := normalise(Render(loadCase(t, name)))…
  16. 16

    Когда достаточно юнит-теста, а когда нужен интеграционный, и как убрать тесты, которым нужен настоящий Postgres, из обычного go test ./...?

    Логику тестируйте юнит-тестами с фейками; интеграционные тесты пишите на то, что код лишь предполагает о другой стороне: SQL, миграции, транзакции, формат данных клиента.

    //go:build integration package store_test // imports: database/sql, os, testing, example.com/store, pgx stdlib…
  17. 17

    Что измеряет go test -cover и почему 100% покрытия не означает, что код хорошо протестирован?

    Покрытие показывает, какие операторы тесты выполнили, а не то, проверили ли что-нибудь.

  18. 18

    Как написать бенчмарк в Go, как выбирается b.N и что меняет b.Loop?

    Бенчмарк представляет собой функцию func BenchmarkXxx(b *testing.B) в файле _test.go, запускается через go test -bench=Xxx; фреймворк повторяет тело, пока прогон не займёт -benchtime (по умолчанию 1s), и выдаёт время на одну операцию.

    func BenchmarkEncode(b *testing.B) {	for _, n := range []int{10, 1000} {		b.Run(fmt.Sprintf("rows=%d", n), func(b *testing.B) {			rows := makeRows(n) // setup: outside b.Loop, not timed			b.ReportAllocs()			for b.Loop() {…
  19. 19

    Парсер заголовков проходит 40 табличных кейсов, но упал с паникой в проде на входе от реального клиента. Чем отличается фазз-тест и когда его стоит добавлять?

    Фазз-тест формулирует свойство, а не пример: f.Fuzz запускает функцию на мутациях исходных входов, направляемых покрытием кода, и добирается до данных, которые никто бы не догадался записать.

    func FuzzParseHeader(f *testing.F) {	// Seeds are the cases you already know. Without -fuzz they run as a	// plain unit test, so CI keeps them forever for free.	f.Add("Bearer abc123")	f.Add("")	f.Add("Bearer \x00\x00")…
  20. 20

    Как сделать так, чтобы тест падал, если тестируемый код допускает утечку горутин?

    Проверьте, какие горутины ещё живы в конце теста: go.uber.org/goleak делает это через defer goleak.VerifyNone(t) в отдельном тесте или goleak.VerifyTestMain(m) для всего пакета и роняет тест со стеками оставшихся горутин.

    // Leaks: the channel is unbuffered, so once First has returned,// the slower senders block on `ch <-` forever.func First(ctx context.Context, urls []string) string {	ch := make(chan string)	for _, u := range urls {		go func() { ch <- fetch(ctx, u) }()…
  21. 21

    Этот бенчмарк показывает 0.25 ns/op при b.N, равном 1000000000. Что он на самом деле измерил и как это исправить?

    func scale(v, f int) int { return v*f + 1 } func BenchmarkScale(b *testing.B) {	for i := 0; i < b.N; i++ {		scale(i, 3)	}}

    Пустой цикл. Компилятор встроил scale, увидел, что результат не используется, и выбросил тело; 0.25 ns/op означает примерно один такт процессора, стоимость инкремента i.

  22. 22

    Найдите баг: по этому бенчмарку 100 000 случайных int сортируются за 0,1 мс, а в продакшене та же сортировка занимает около 10 мс.

    func BenchmarkSort(b *testing.B) {	data := rand.Perm(100_000) // shuffled 0..99999	for b.Loop() {		slices.Sort(data)	}}

    slices.Sort сортирует data на месте, поэтому перемешанный вход видит только первая итерация; все следующие сортируют уже отсортированный срез, и pdqsort справляется с ним почти за линейное время.

  23. 23

    Один прогон go test -bench до изменения и один после показывают ускорение на 9%. Почему это ничего не доказывает и как выглядит корректное сравнение?

    Один прогон на сторону даёт одну выборку.

  24. 24

    Как тестировать код с таймаутами, тикерами и фоновыми горутинами без реального ожидания и без внедрения часов, и что делает testing/synctest?

    synctest.Test (Go 1.25) запускает функцию теста в «пузыре» с фейковыми часами: время внутри идёт только тогда, когда все горутины пузыря заблокированы, поэтому минутный таймаут проходит мгновенно и детерминированно, а synctest.Wait() ждёт, пока фоновые горутины успокоятся.

    func TestCacheEvictsAfterTTL(t *testing.T) {	synctest.Test(t, func(t *testing.T) {		c := NewCache(time.Minute) // starts a janitor goroutine with a Ticker		defer c.Close()            // the janitor must exit before Test returns		c.Set("k", "v")…
  25. 25

    Примерно каждый двадцатый прогон CI падает: каждый раз другой тест, при перезапуске всё зелёное. Команда начала добавлять ретраи. Как превратить это в баг, который можно починить?

    Ретраи прячут баг; вместо этого сделайте набор тестов враждебным.