Irbisa · Шпаргалка
Тестирование и бенчмарки
Табличные тесты, httptest, бенчмарки, фаззинг, -race в CI
Как
go testнаходит и запускает тесты и что делают./...,-run,-vи-count=1?Что такое табличный тест в Go и что добавляет обёртка каждого кейса в
t.Runпо сравнению с обычным цикломfor?func TestParseDuration(t *testing.T) { cases := []struct { name string in string want time.Duration wantErr bool…Чем отличаются
t.Errorиt.Fatalи когда использовать каждый?Зачем тестовый хелпер начинается с
t.Helper()и почему его первый параметр обычноtesting.TB, а не*testing.T?// 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 {…Почему некоторые тестовые файлы объявляют
package foo_test, а неpackage 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…Что такое функция
Exampleс комментарием// Output:и какgo testеё проверяет?package money_test import ( "fmt" "example.com/money"…Что умеет
t.Cleanupтакого, чего не можетdeferвнутри того же хелпера, и когдаdeferвсё-таки лучше?// 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() }}…Что такое
TestMainи когда он нужен вместо подготовки в каждом тесте?var testDB *sql.DB func TestMain(m *testing.M) { flag.Parse() // needed before testing.Short() or custom flags if testing.Short() { m.Run()…Этот тест зависает до 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()}При
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) } })}Найдите баг: этот табличный тест остался зелёным после изменения, из-за которого
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) } }) }}Когда тестировать через
httptest.NewRecorder, а когда черезhttptest.NewServer, и чего рекордер не воспроизводит?// 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()…Как покрыть юнит-тестами сервис, который ходит в базу и во внешний API, не поднимая ни то ни другое, и писать фейки руками или генерировать моки?
// 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}…Тест на истечение токена спит две секунды и всё равно падает на CI примерно раз в неделю. Как проверить истечение, не завися от реальных часов?
type Session struct { expires time.Time now func() time.Time // the seam} func NewSession(ttl time.Duration, now func() time.Time) *Session {…В тесте рендерера зашита ожидаемая строка на 200 строк, которая ломается при каждой правке шаблона. Как перейти на golden-файлы и не получить тест, который никогда не падает?
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)))…Когда достаточно юнит-теста, а когда нужен интеграционный, и как убрать тесты, которым нужен настоящий Postgres, из обычного
go test ./...?//go:build integration package store_test // imports: database/sql, os, testing, example.com/store, pgx stdlib…Что измеряет
go test -coverи почему 100% покрытия не означает, что код хорошо протестирован?Как написать бенчмарк в Go, как выбирается
b.Nи что меняетb.Loop?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() {…Парсер заголовков проходит 40 табличных кейсов, но упал с паникой в проде на входе от реального клиента. Чем отличается фазз-тест и когда его стоит добавлять?
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")…Как сделать так, чтобы тест падал, если тестируемый код допускает утечку горутин?
// 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) }()…Этот бенчмарк показывает
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) }}Найдите баг: по этому бенчмарку 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) }}Один прогон
go test -benchдо изменения и один после показывают ускорение на 9%. Почему это ничего не доказывает и как выглядит корректное сравнение?Как тестировать код с таймаутами, тикерами и фоновыми горутинами без реального ожидания и без внедрения часов, и что делает
testing/synctest?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")…Примерно каждый двадцатый прогон CI падает: каждый раз другой тест, при перезапуске всё зелёное. Команда начала добавлять ретраи. Как превратить это в баг, который можно починить?