18. Testing — стандартный пакет `testing`
Полный разбор тестирования в Go: от
t.Errorдо фаззинга иsynctest. Зачем это знать: тесты — это контракт качества кода. Junior, который умеет писать табличные тесты сt.Parallel()и понимаетt.Helper(), выглядит на собесе ощутимо сильнее.
Содержание
Заголовок раздела «Содержание»1. Базовое API
Заголовок раздела «1. Базовое API»1.1. Стандартный пакет testing
Заголовок раздела «1.1. Стандартный пакет testing»Go идёт с встроенным тестовым фреймворком — пакет testing. Никаких сторонних библиотек для базы не нужно. Запуск — командой go test.
package math
func Add(a, b int) int { return a + b}package math
import "testing"
func TestAdd(t *testing.T) { if Add(2, 3) != 5 { t.Errorf("Add(2, 3) = %d; want 5", Add(2, 3)) }}go test ./...1.2. File convention: *_test.go
Заголовок раздела «1.2. File convention: *_test.go»Файлы тестов имеют суффикс _test.go. Они:
- Включаются в сборку только при
go test. - Не попадают в финальный бинарь.
- Могут находиться в том же пакете (
package math) или вpackage math_test(внешний тест).
Внутренние vs внешние тесты
Заголовок раздела «Внутренние vs внешние тесты»package math
// доступны приватные функцииpackage math_test
// доступен только публичный APIВнешние тесты заставляют тестировать как клиент — это лучше с точки зрения чистоты API. Часто их совмещают: интеграционные/black-box в _test подпакете.
1.3. Function convention: TestXxx(t *testing.T)
Заголовок раздела «1.3. Function convention: TestXxx(t *testing.T)»func TestAdd(t *testing.T) { ... }func TestEdgeCases(t *testing.T) { ... }Правила:
- Имя начинается с
Test. - После
Test— заглавная буква. - Один аргумент
*testing.T. - Регистрируется автоматически.
Аналогично:
BenchmarkXxx(b *testing.B)— бенчмарки.ExampleXxx()— примеры.FuzzXxx(f *testing.F)— фаззинг.TestMain(m *testing.M)— setup/teardown.
1.4. t.Error vs t.Fatal
Заголовок раздела «1.4. t.Error vs t.Fatal»func TestThings(t *testing.T) { if x != expected { t.Errorf("got %v, want %v", x, expected) // продолжает тест } if y != expected { t.Fatalf("got %v, want %v", y, expected) // останавливает функцию } fmt.Println("сюда дойдём, только если первый Error, но не Fatal")}| Метод | Помечает тест failed | Продолжает выполнение |
|---|---|---|
t.Log / t.Logf | нет | да |
t.Error / t.Errorf | да | да |
t.Fatal / t.Fatalf | да | нет (runtime.Goexit) |
t.Skip / t.Skipf | помечает skipped | нет |
t.SkipNow | помечает skipped | нет |
t.FailNow | да | нет |
Важно: t.Fatal останавливает функцию, в которой вызвана. Это означает:
- В вспомогательной функции (helper)
t.Fatalостановит helper, но не сам тест. - Поэтому нужен
t.Helper(), чтобы правильно сообщать line numbers.
t.Fatal использует runtime.Goexit, который вызывает все defer правильно. Это не паника.
1.5. t.Log, t.Skip
Заголовок раздела «1.5. t.Log, t.Skip»t.Logf("got x=%d", x) // печатается только при -v или failt.Skipf("skipping: %s reason", "no env var")t.Log пишет в лог теста. По умолчанию не отображается, только при -v или если тест провалился.
t.Skip пометит тест как SKIP. Часто используется для conditional skip:
func TestIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in -short mode") } // ...}1.6. Table-driven tests
Заголовок раздела «1.6. Table-driven tests»Каноническая форма тестов в Go:
func TestAdd(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive", 2, 3, 5}, {"zero", 0, 0, 0}, {"negative", -1, -2, -3}, {"overflow", math.MaxInt, 1, math.MinInt}, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := Add(tt.a, tt.b) if got != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected) } }) }}Преимущества:
- Легко добавлять кейсы.
t.Runсоздаёт subtest: видно, какой кейс упал.- Можно фильтровать:
go test -run TestAdd/positive.
1.7. t.Run для subtests
Заголовок раздела «1.7. t.Run для subtests»t.Run("subname", func(t *testing.T) { // подтест})Subtest имеет свой *testing.T и счётчик. Можно вкладывать:
t.Run("group1", func(t *testing.T) { t.Run("case1", func(t *testing.T) {}) t.Run("case2", func(t *testing.T) {})})Фильтр запуска через -run:
go test -run TestAdd # все subtests TestAddgo test -run TestAdd/positive # только positivego test -run "TestAdd/.*ative" # regex1.8. t.Parallel() — параллельные тесты
Заголовок раздела «1.8. t.Parallel() — параллельные тесты»func TestSomething(t *testing.T) { t.Parallel() // ...}Помечает тест как параллельный. Все тесты с t.Parallel() сначала “паркуются”, потом запускаются параллельно с другими t.Parallel().
Параллелизм:
- Ограничен
GOMAXPROCS(можно-parallel N). - Между разными pkg всегда параллельно (можно
-p N). - Внутри одного pkg — только если есть
t.Parallel().
go test -parallel 4 ./...1.9. Loop variable trap в subtests
Заголовок раздела «1.9. Loop variable trap в subtests»До Go 1.22:
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() // BUG: tt захвачен по ссылке (одна переменная)! // Все горутины увидят последнее значение. _ = tt })}Лекарство:
for _, tt := range tests { tt := tt // shadowing — каждая итерация своя tt t.Run(tt.name, func(t *testing.T) { t.Parallel() _ = tt })}С Go 1.22: loop variables в for range имеют per-iteration scope. Баг не воспроизводится. Можно писать чисто:
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() _ = tt // уже корректно })}Но если поддерживаешь Go < 1.22 — продолжай делать tt := tt.
1.10. t.Helper() — корректные line numbers
Заголовок раздела «1.10. t.Helper() — корректные line numbers»func assertEqual(t *testing.T, got, want int) { t.Helper() // отметить как helper if got != want { t.Errorf("got %d; want %d", got, want) }}
func TestSomething(t *testing.T) { assertEqual(t, Add(2, 3), 5) // ← report укажет ЭТУ строку}Без t.Helper() сообщение об ошибке указало бы на строку внутри assertEqual, а не на ту, что в TestSomething. С t.Helper() фрейм helper’а исключается из traceback.
t.Helper() можно вызывать в любой функции, включая методы. Эффект — пока helper-функция активна.
1.11. Setup/Teardown
Заголовок раздела «1.11. Setup/Teardown»TestMain(m *testing.M)
Заголовок раздела «TestMain(m *testing.M)»Глобальная setup/teardown для всего тестового пакета:
func TestMain(m *testing.M) { // SETUP setupDB()
code := m.Run() // запустить все тесты
// TEARDOWN teardownDB()
os.Exit(code)}Важно: os.Exit пропускает defer. Поэтому teardown — перед os.Exit.
t.Cleanup() — per-test (Go 1.14+)
Заголовок раздела «t.Cleanup() — per-test (Go 1.14+)»func TestThing(t *testing.T) { server := startServer() t.Cleanup(func() { server.Stop() }) // ...}t.Cleanup регистрирует callback, вызываемый при завершении теста (даже при t.Fatal). Можно несколько раз — выполнятся в LIFO порядке.
Преимущества t.Cleanup над defer:
- Работает в subtests: cleanup конкретного subtest, не всего теста.
- Гарантированно запускается при
t.FailNow/t.Fatal. - Работает поверх
t.Parallel()корректно.
1.12. Fixtures: testdata папка
Заголовок раздела «1.12. Fixtures: testdata папка»mypkg/ parser.go parser_test.go testdata/ input1.json expected1.jsonПапка testdata — специальная для Go: её содержимое игнорируется компилятором, но доступно тестам.
func TestParse(t *testing.T) { data, err := os.ReadFile("testdata/input1.json") if err != nil { t.Fatal(err) } got := Parse(data)
want, _ := os.ReadFile("testdata/expected1.json") if !bytes.Equal(got, want) { t.Errorf("mismatch") }}Golden files
Заголовок раздела «Golden files»Шаблон “записать ожидаемый результат в файл, потом сравнивать”:
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) { got := Render(input) golden := filepath.Join("testdata", "render.golden")
if *update { os.WriteFile(golden, got, 0644) }
want, _ := os.ReadFile(golden) if !bytes.Equal(got, want) { t.Errorf("render mismatch") }}Запуск:
go test -update # обновить golden filesgo test # проверка1.13. Coverage
Заголовок раздела «1.13. Coverage»go test -cover ./...# coverage: 73.2% of statementsСохранить в файл:
go test -coverprofile=coverage.out ./...Посмотреть отчёт:
go tool cover -func=coverage.outgo tool cover -html=coverage.out # открывает в браузереТакже есть режимы:
go test -covermode=count # count = сколько раз каждая строкаgo test -covermode=atomic # для параллельных тестовgo test -covermode=set # set = просто покрыта (default)С Go 1.20+ покрытие можно собирать с бинарей, не только с тестов:
go build -cover -o myappGOCOVERDIR=/tmp/cov ./myappgo tool covdata textfmt -i=/tmp/cov -o=cov.txt1.14. Benchmarks
Заголовок раздела «1.14. Benchmarks»func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { _ = Add(2, 3) }}b.N — счётчик итераций. Go сам подбирает его, чтобы бенчмарк длился ~1 секунду.
Запуск:
go test -bench=. # все бенчмаркиgo test -bench=BenchmarkAdd # одинgo test -bench=. -benchmem # с allocation statsgo test -bench=. -benchtime=10s # дольшеgo test -bench=. -benchtime=100x # ровно 100 итерацийgo test -bench=. -count=5 # повторить 5 разПример вывода:
BenchmarkAdd-8 1000000000 0.4032 ns/op 0 B/op 0 allocs/op1000000000—b.N(количество итераций).0.4032 ns/op— наносекунды на одну итерацию.0 B/op— байт аллоцировано на итерацию.0 allocs/op— количество аллокаций.
b.ResetTimer(), b.StopTimer(), b.StartTimer()
Заголовок раздела «b.ResetTimer(), b.StopTimer(), b.StartTimer()»Когда есть тяжёлая инициализация:
func BenchmarkExpensive(b *testing.B) { data := setupLargeData() // не должно входить в измерение b.ResetTimer()
for i := 0; i < b.N; i++ { Process(data) }}b.StopTimer()/b.StartTimer() — для измерений с переменной нагрузкой:
for i := 0; i < b.N; i++ { b.StopTimer() data := generateNew() b.StartTimer()
Process(data)}b.ReportAllocs()
Заголовок раздела «b.ReportAllocs()»func BenchmarkFoo(b *testing.B) { b.ReportAllocs() // эквивалент флагу -benchmem для этого бенча for i := 0; i < b.N; i++ { _ = make([]byte, 1024) }}Sub-benchmarks с b.Run
Заголовок раздела «Sub-benchmarks с b.Run»func BenchmarkSizes(b *testing.B) { for _, n := range []int{10, 100, 1000} { b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) { for i := 0; i < b.N; i++ { Process(n) } }) }}benchstat для сравнения
Заголовок раздела «benchstat для сравнения»Установить:
go install golang.org/x/perf/cmd/benchstat@latestgo test -bench=. -count=10 > old.txt# ... изменения ...go test -bench=. -count=10 > new.txtbenchstat old.txt new.txtВывод покажет процентное изменение и статистическую значимость.
1.15. Examples — компилируемые примеры
Заголовок раздела «1.15. Examples — компилируемые примеры»func ExampleAdd() { fmt.Println(Add(2, 3)) // Output: 5}go test запустит ExampleAdd, захватит stdout и сравнит с // Output: блоком. Если не совпало — fail.
Преимущества:
- Документация, которая не врёт (компилируется).
- Появляется в godoc.
Типы:
ExampleAdd— функцияAdd.ExampleAdd_simple— вариант дляAdd.Example_global— пример пакета.
Unordered output
Заголовок раздела «Unordered output»func ExampleMapKeys() { m := map[string]int{"a": 1, "b": 2} for k := range m { fmt.Println(k) } // Unordered output: // a // b}1.16. Fuzz testing (Go 1.18+)
Заголовок раздела «1.16. Fuzz testing (Go 1.18+)»Фаззинг — автоматическая генерация входных данных:
func FuzzReverse(f *testing.F) { // Seed corpus — начальные данные f.Add("hello") f.Add("") f.Add("¥¥¥")
f.Fuzz(func(t *testing.T, s string) { rev := Reverse(s) revRev := Reverse(rev) if s != revRev { t.Errorf("Reverse(Reverse(%q)) = %q; want %q", s, revRev, s) } if utf8.ValidString(s) && !utf8.ValidString(rev) { t.Errorf("invalid UTF-8: %q -> %q", s, rev) } })}Запуск:
go test -fuzz=FuzzReverse # фаззингgo test -fuzz=FuzzReverse -fuzztime=30sgo test -fuzz=. -run=^$ # только фаззинг, без unit-тестовКогда находит контр-пример, сохраняет в testdata/fuzz/FuzzReverse/<hash>. После этого go test (без -fuzz) автоматически прогонит этот файл как unit-тест.
Поддерживаемые типы аргументов
Заголовок раздела «Поддерживаемые типы аргументов»string, []byte, целочисленные типы (int, int8.., uint8..), float32, float64, bool, rune. Структуры не поддерживаются.
1.17. httptest пакет
Заголовок раздела «1.17. httptest пакет»httptest.NewServer
Заголовок раздела «httptest.NewServer»import "net/http/httptest"
func TestClient(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"ok":true}`)) })) defer server.Close()
resp, err := http.Get(server.URL) if err != nil { t.Fatal(err) } defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) if string(body) != `{"ok":true}` { t.Errorf("wrong body: %s", body) }}httptest.NewServer слушает на случайном порту — на 127.0.0.1.
httptest.NewTLSServer — то же с самоподписанным TLS.
httptest.NewRecorder
Заголовок раздела «httptest.NewRecorder»Для теста HTTP-хендлеров без запуска сервера:
func MyHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) fmt.Fprintln(w, "hello")}
func TestMyHandler(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder()
MyHandler(w, req)
resp := w.Result() body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 { t.Errorf("status = %d; want 200", resp.StatusCode) } if !strings.Contains(string(body), "hello") { t.Errorf("body = %s", body) }}1.18. testing/iotest пакет
Заголовок раздела «1.18. testing/iotest пакет»Утилиты для тестирования I/O:
import "testing/iotest"
// Reader, который возвращает данные по одному байтуr := iotest.OneByteReader(strings.NewReader("hello"))
// Reader, который возвращает ошибку после N байтr := iotest.TimeoutReader(strings.NewReader("hello"))
// Reader, который читает половинуr := iotest.HalfReader(strings.NewReader("hello"))
// Reader, в котором каждый Read возвращает err после данныхr := iotest.DataErrReader(strings.NewReader("hello"))Хорошо для проверки, что код читает в цикле и обрабатывает короткие чтения.
1.19. testing/synctest (Go 1.24+, experimental)
Заголовок раздела «1.19. testing/synctest (Go 1.24+, experimental)»В Go 1.24 появился экспериментальный пакет testing/synctest (требует GOEXPERIMENT=synctest). Решает проблему тестов с временем.
import "testing/synctest"
func TestTimeout(t *testing.T) { synctest.Run(func() { ch := make(chan int) go func() { time.Sleep(time.Hour) ch <- 42 }() // synctest.Wait() — ждём, пока все горутины запаркуются synctest.Wait() // время "прошло" мгновенно })}Внутри synctest.Run время фейковое. Полезно для:
- Таймеров без
time.Sleep. - Детерминированных тестов с горутинами.
- Тестов с дедлайнами.
Пока экспериментально — API может меняться.
1.20. Race detector в тестах
Заголовок раздела «1.20. Race detector в тестах»go test -race ./...Включает race detector — динамически проверяет конкурентный доступ к памяти. Замедляет ~5-10x, поэтому обычно только в CI или отдельно.
Каждый race репортит:
- Где первая горутина пишет.
- Где вторая читает/пишет.
- Stack trace создания горутин.
1.21. Mock библиотеки (кратко)
Заголовок раздела «1.21. Mock библиотеки (кратко)»В standard library моков нет. Популярны:
gomock (от Google)
Заголовок раздела «gomock (от Google)»go install go.uber.org/mock/mockgen@latestmockgen -source=user.go -destination=mock_user.goctrl := gomock.NewController(t)defer ctrl.Finish()
mockRepo := NewMockRepo(ctrl)mockRepo.EXPECT().Get(gomock.Any()).Return(&User{ID: 1}, nil)testify/mock
Заголовок раздела «testify/mock»import "github.com/stretchr/testify/mock"
type MockRepo struct { mock.Mock}
func (m *MockRepo) Get(id int) (*User, error) { args := m.Called(id) return args.Get(0).(*User), args.Error(1)}
// в тестеm := new(MockRepo)m.On("Get", 1).Return(&User{ID: 1}, nil)Junior’у достаточно знать, что такие либы существуют. Middle — уметь применять.
1.22. testify/assert и testify/require
Заголовок раздела «1.22. testify/assert и testify/require»import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require")
func TestStuff(t *testing.T) { assert.Equal(t, 5, Add(2, 3)) // tests continues require.NoError(t, err) // tests stops on fail assert.Contains(t, "hello world", "world")}assert.*— какt.Error(продолжает).require.*— какt.Fatal(останавливает).
В РФ часто используется testify. Но в стандарте Go его нет, и некоторые команды (особенно из core Go-сообщества) предпочитают чистый testing + helper’ы.
1.23. Полезные флаги go test
Заголовок раздела «1.23. Полезные флаги go test»go test -v # verbose, видеть имена тестов и Loggo test -run TestFoo # фильтр по имениgo test -count=10 # запустить N разgo test -timeout=30s # таймаут на тестgo test -short # пометить как short mode (testing.Short())go test -race # race detectorgo test -cover # покрытиеgo test -bench=. # бенчмаркиgo test -fuzz=. # фаззингgo test -failfast # остановиться при первом failgo test -parallel=4 # ограничение параллелизмаgo test ./... # все пакеты рекурсивно2. Под капотом
Заголовок раздела «2. Под капотом»2.1. Как go test находит тесты
Заголовок раздела «2.1. Как go test находит тесты»go testпарсит все*_test.goв пакете.- Генерирует временный файл
_testmain.goсfunc main(). - Этот
main()вызываетtesting.Mainсо списком найденныхTestXxx/BenchmarkXxx/ExampleXxx/FuzzXxx. - Собирается бинарь
<pkg>.test. - Запускается с переданными аргументами.
Можно увидеть бинарь:
go test -c -o mytest./mytest -test.v -test.run=TestFooФлаги для бинаря имеют префикс -test. (например, -test.run), а go test принимает их без префикса.
2.2. testing.T и testing.B структура
Заголовок раздела «2.2. testing.T и testing.B структура»type T struct { common isParallel bool isEnvSet bool context *testContext}
type common struct { mu sync.RWMutex output []byte w io.Writer ran bool failed bool skipped bool done bool helperPCs map[uintptr]struct{} helperNames map[string]struct{} cleanups []func() // ...}Внутри:
mu— мьютекс для thread-safe вызовов из горутин (например, вt.Parallel()).helperPCs— список program counters, помеченныхt.Helper().cleanups— стек callback’ов отt.Cleanup().failed— флаг “тест провалился”.
2.3. Как работает t.Parallel()
Заголовок раздела «2.3. Как работает t.Parallel()»При вызове t.Parallel():
- Тест помечается как параллельный.
- Текущий goroutine блокируется на сигнал.
- После того как все sequential тесты в пакете завершатся, parallel-тесты разблокируются.
- Они выполняются параллельно с лимитом
GOMAXPROCS(или-parallel).
Псевдокод:
func (t *T) Parallel() { t.signal <- struct{}{} // сигнал родителю "можно запускать следующий" <-t.context.startParallel // ждать сигнала на старт}Поэтому в subtests с t.Parallel() важно: outer test не закончится, пока все parallel children не завершатся.
2.4. Алгоритм выбора b.N
Заголовок раздела «2.4. Алгоритм выбора b.N»Iteration 1: b.N = 1 → измерили T1Iteration 2: b.N = max(2 * predicted, 1) → predicted = b.N * (benchtime / T1)Iteration 3: ...Stop when accumulated time >= benchtimeВ реальности — итеративно растёт (1 → 100 → 10000 → …), пока общее время не превысит -benchtime (по умолчанию 1s).
2.5. Coverage instrumentation
Заголовок раздела «2.5. Coverage instrumentation»При -cover:
- Go-компилятор инструментирует бинарь — в каждом basic block добавляется счётчик.
- Тесты запускают код, счётчики увеличиваются.
- По завершению — счётчики сохраняются и анализируются.
Modes:
set— bool: “был выполнен или нет”.count— int: “сколько раз”.atomic—int64с atomic-ops, для race-safe.
С Go 1.20+ — отдельный механизм для бинарей (не только тестов):
go build -cover -o myappGOCOVERDIR=/tmp/cov ./myapp2.6. Как t.Helper() работает
Заголовок раздела «2.6. Как t.Helper() работает»t.Helper() запоминает PC (program counter) текущей функции в helperPCs. Когда тест печатает ошибку, он идёт по stack frames вниз и пропускает все, чьи PC помечены как helper. Первый не-helper фрейм — это и есть “место вызова”.
func (c *common) Helper() { pc, _, _, _ := runtime.Caller(1) c.helperPCs[pc] = struct{}{}}2.7. t.Cleanup vs defer
Заголовок раздела «2.7. t.Cleanup vs defer»t.Cleanup функции:
- Запускаются в LIFO порядке.
- Запускаются даже после
t.FailNow/t.Fatal(deferтоже, ноruntime.Goexit). - Запускаются после завершения subtest, не всего теста.
func TestParent(t *testing.T) { t.Cleanup(func() { fmt.Println("parent cleanup") })
t.Run("child", func(t *testing.T) { t.Cleanup(func() { fmt.Println("child cleanup") }) })
// ChildCleanup напечатается ДО ParentCleanup (LIFO + scoped to subtest)}2.8. Что такое testing.Short()
Заголовок раздела «2.8. Что такое testing.Short()»Это просто bool флаг, выставляемый через -short:
if testing.Short() { t.Skip("skipping integration tests in short mode")}Часто make test-unit использует -short, а make test-integration — без.
2.9. Race detector
Заголовок раздела «2.9. Race detector»Race detector — это ThreadSanitizer (TSan) от Google, портированный для Go. Включается флагом -race:
go test -race ./...go build -race -o myappgo run -race main.goНакладные расходы:
- Память: 5-10x.
- CPU: 2-20x (зависит от паттерна).
Не подходит для prod, только для тестирования.
2.10. Fuzzing engine
Заголовок раздела «2.10. Fuzzing engine»go test -fuzz запускает coverage-guided фаззинг. Worker’ы:
- Берут seed corpus.
- Применяют мутации (битовые флипы, добавление/удаление байт, и т.д.).
- Сравнивают coverage с предыдущими прогонами.
- Сохраняют “интересные” входы (увеличившие coverage).
- При краше сохраняют в
testdata/fuzz/FuzzXxx/<hash>.
2.11. Internal: как t.Run создаёт subtest
Заголовок раздела «2.11. Internal: как t.Run создаёт subtest»func (t *T) Run(name string, f func(t *T)) bool { t.hasSub.Store(true) sub := &T{ common: common{ name: t.name + "/" + rewrite(name), parent: &t.common, level: t.level + 1, }, context: t.context, } // ... запускает f в собственной горутине}Subtest — это новый *testing.T, новая goroutine, отдельный счётчик failures. Имя формируется из родителя + / + имя.
3. Gotchas
Заголовок раздела «3. Gotchas»3.1. Loop variable trap до Go 1.22
Заголовок раздела «3.1. Loop variable trap до Go 1.22»for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() _ = tt // BUG до Go 1.22: общая переменная })}С Go 1.22 баг исправлен (per-iteration scope). Если поддерживаешь старые версии — tt := tt.
3.2. t.Parallel() и race condition с shared state
Заголовок раздела «3.2. t.Parallel() и race condition с shared state»var counter int
func TestA(t *testing.T) { t.Parallel() counter++ // race с TestB}
func TestB(t *testing.T) { t.Parallel() counter++}-race найдёт это сразу. Не использовать shared mutable state в parallel-тестах.
3.3. Запуск тестов через go test ./pkg vs go test ./...
Заголовок раздела «3.3. Запуск тестов через go test ./pkg vs go test ./...»go test ./pkg # тесты pkg/go test ./... # все пакеты рекурсивно./... обходит подпапки. Часто можно случайно зацепить третейские модули, если не настроен go.work.
3.4. os.Exit в TestMain пропускает defer
Заголовок раздела «3.4. os.Exit в TestMain пропускает defer»func TestMain(m *testing.M) { defer cleanup() // ✗ не вызовется os.Exit(m.Run())}Правильно:
func TestMain(m *testing.M) { code := m.Run() cleanup() // явно os.Exit(code)}3.5. t.Fatal в горутине не останавливает тест
Заголовок раздела «3.5. t.Fatal в горутине не останавливает тест»func TestThing(t *testing.T) { go func() { t.Fatal("oops") // ⚠️ panic: Fatal called from non-test goroutine }() // ...}t.Fatal/t.FailNow нельзя вызывать из другой горутины. Только из той, где запущен тест. Используй каналы для передачи ошибок:
errCh := make(chan error, 1)go func() { if x != y { errCh <- fmt.Errorf("bad") return } errCh <- nil}()if err := <-errCh; err != nil { t.Fatal(err)}3.6. Coverage не = качество тестов
Заголовок раздела «3.6. Coverage не = качество тестов»func Divide(a, b int) int { return a / b}
func TestDivide(t *testing.T) { _ = Divide(10, 2) // 100% coverage, но ничего не проверяет}Coverage показывает, какие строки выполнены, но не утверждает корректность. Полагайся на assertions + property-based + edge cases.
3.7. Generated code в coverage
Заголовок раздела «3.7. Generated code в coverage»go test -cover считает coverage по всему коду, включая сгенерированный (например, easyjson, mockgen). Это занижает реальный coverage написанного человеком кода.
Решения:
-coverpkg— указать пакеты для подсчёта.- Исключать сгенерированный код через build tags (
//go:build !generated).
3.8. Examples без // Output: — не запускаются
Заголовок раздела «3.8. Examples без // Output: — не запускаются»func ExampleAdd() { fmt.Println(Add(2, 3)) // забыли // Output:}Этот пример скомпилируется (т.е. ошибки компиляции поймаются), но не запустится. Чтобы Go реально выполнил его и проверил, нужен комментарий // Output: 5.
3.9. Test caching
Заголовок раздела «3.9. Test caching»go test ./...# во второй раз:go test ./...ok github.com/example/foo (cached)Go кэширует результаты тестов: если ни код, ни тесты не менялись, кэш возвращается. Сбросить:
go clean -testcache# илиgo test -count=1 ./...3.10. httptest.NewServer — порт
Заголовок раздела «3.10. httptest.NewServer — порт»httptest.NewServer слушает на 127.0.0.1:server.URL. Не пытайся хардкодить порт — он рандомный.
3.11. t.Cleanup callback вызывается даже при panic?
Заголовок раздела «3.11. t.Cleanup callback вызывается даже при panic?»Нет — t.Cleanup вызывается на t.FailNow (через runtime.Goexit). Но panic тест убьёт без cleanup, если паника не восстановлена. Поэтому в TestMain или в helper’ах иногда используют recover.
3.12. t.Parallel() после t.Run
Заголовок раздела «3.12. t.Parallel() после t.Run»t.Run("a", func(t *testing.T) { t.Parallel() // ...})t.Parallel() внутри subtest означает: этот subtest параллелен другим subtest’ам того же родителя. Хороший паттерн для параллельных табличных тестов.
3.13. Бенчмарк с b.N < 1
Заголовок раздела «3.13. Бенчмарк с b.N < 1»В современном Go это невозможно — b.N >= 1 гарантировано. Но если у тебя бенч с if b.N == 0 { ... } — это мёртвый код.
3.14. Бенчмарк, который компилятор оптимизирует в ничто
Заголовок раздела «3.14. Бенчмарк, который компилятор оптимизирует в ничто»func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(2, 3) // результат отбрасывается → компилятор может удалить }}Защита:
var sink int
func BenchmarkAdd(b *testing.B) { var x int for i := 0; i < b.N; i++ { x = Add(2, 3) } sink = x // глобальная переменная — компилятор не оптимизирует}Или использовать runtime.KeepAlive.
3.15. testdata папка — особая
Заголовок раздела «3.15. testdata папка — особая»mypkg/testdata/...Эта папка:
- Не индексируется компилятором (как
_или.). - Не считается пакетом.
- Не попадает в
go list ./....
Не путать с обычной папкой — там Go-файлы будут собраны.
3.16. Race detector только cgo/amd64/arm64/ppc64le
Заголовок раздела «3.16. Race detector только cgo/amd64/arm64/ppc64le»-race требует cgo и работает только на ограниченных платформах (amd64, arm64, mips64, ppc64le, s390x на Linux/Mac/Windows). На Android/iOS — нет.
3.17. -run matching: подстрока, не имя
Заголовок раздела «3.17. -run matching: подстрока, не имя»go test -run AddЗапустит все тесты, чьё имя содержит Add: TestAdd, TestAddNegative, TestSubtractAdd. Используй regex для точности:
go test -run '^TestAdd$'3.18. Subtest names ”/” эскейпятся
Заголовок раздела «3.18. Subtest names ”/” эскейпятся»t.Run("a/b", func(t *testing.T) { ... }) // в выводе будет "a/b" с escaped /Имена subtest’ов с / стрипаются — / имеет специальное значение для -run. Лучше избегать.
3.19. testing.B.RunParallel
Заголовок раздела «3.19. testing.B.RunParallel»func BenchmarkParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { // работа } })}Запускает бенч в GOMAXPROCS горутин. Полезно для тестирования конкурентного кода. Не использовать с обычным for i := 0; i < b.N; i++ — это разные API.
3.20. Fuzz seed corpus в testdata
Заголовок раздела «3.20. Fuzz seed corpus в testdata»Файлы в testdata/fuzz/FuzzXxx/ загружаются автоматически как seed. При go test (без -fuzz) они запускаются как unit-тесты, чтобы регрессии не вернулись.
4. Best practices
Заголовок раздела «4. Best practices»4.1. Hermetic tests
Заголовок раздела «4.1. Hermetic tests»Тесты должны быть изолированы:
- Не лазить в сеть.
- Не зависеть от файловой системы (кроме
testdata). - Не использовать глобальное состояние.
- Не зависеть от порядка выполнения.
Тесты, которые сегодня прошли — должны проходить через год.
4.2. Fast tests
Заголовок раздела «4.2. Fast tests»go test ./... должен быть быстрым — секунды, не минуты. Иначе разработчики перестанут запускать локально.
- Mock внешние сервисы.
- Используй in-memory implementations (например,
sqlite:memory:). - Параллель там, где можно.
Долгие тесты — за флагом -short:
if testing.Short() { t.Skip("skipping integration in -short")}4.3. Parallel where possible
Заголовок раздела «4.3. Parallel where possible»func TestA(t *testing.T) { t.Parallel() // ...}Если нет shared state — добавляй t.Parallel(). Это найдёт race-condition’ы и ускорит запуск.
4.4. Avoid shared state
Заголовок раздела «4.4. Avoid shared state»// плохоvar globalDB *DB
func TestA(t *testing.T) { globalDB.Query(...)}Каждый тест — свой setup. Используй t.Cleanup или TestMain.
4.5. Table-driven tests as default
Заголовок раздела «4.5. Table-driven tests as default»Если есть >2 случая — пиши таблицу:
tests := []struct { name string input ... want ...}{ // ...}for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // ... })}4.6. t.Helper() в каждом helper’е
Заголовок раздела «4.6. t.Helper() в каждом helper’е»Любая функция, которая принимает *testing.T и зовёт t.Error/Fatal — должна начинать с t.Helper().
4.7. Описательные имена
Заголовок раздела «4.7. Описательные имена»// плохоfunc TestAdd(t *testing.T) {}
// лучшеfunc TestAdd_PositiveNumbers(t *testing.T) {}func TestAdd_OverflowReturnsMinInt(t *testing.T) {}Или с table-driven:
{name: "overflow returns min int", a: MaxInt, b: 1, want: MinInt},4.8. Не комментируй сложные кейсы — пиши // edge case: ...
Заголовок раздела «4.8. Не комментируй сложные кейсы — пиши // edge case: ...»{ name: "empty string", // edge case: должно вернуть пустую строку, а не nil input: "", want: "",},4.9. Не дублируй setup в каждом тесте
Заголовок раздела «4.9. Не дублируй setup в каждом тесте»func setupTest(t *testing.T) (*Server, *Client) { t.Helper() s := startServer() t.Cleanup(s.Stop) c := newClient(s.URL) return s, c}
func TestA(t *testing.T) { s, c := setupTest(t) // ...}4.10. Бенч с разными размерами
Заголовок раздела «4.10. Бенч с разными размерами»Всегда параметризуй бенчи:
func BenchmarkSort(b *testing.B) { for _, n := range []int{10, 100, 1000, 10000} { b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) { data := make([]int, n) b.ResetTimer() for i := 0; i < b.N; i++ { Sort(data) } }) }}4.11. -race в CI
Заголовок раздела «4.11. -race в CI»Минимум один CI job должен запускать тесты с -race. Race condition’ы дёшево ловятся в тестах, дорого — в проде.
4.12. -count=1 для надёжности
Заголовок раздела «4.12. -count=1 для надёжности»Кэш go test помогает в локальной разработке, но в CI лучше -count=1, чтобы убедиться, что всё реально выполнилось.
4.13. Один пакет = один уровень тестов
Заголовок раздела «4.13. Один пакет = один уровень тестов»Не смешивай unit и integration в одном пакете. Делай:
pkg/parser/parser.go+parser_test.go— unit.pkg/parser/integration_test.goс build tag — integration.
//go:build integration
package parsergo test -tags=integration ./...4.14. Используй testify/require для setup
Заголовок раздела «4.14. Используй testify/require для setup»Когда setup провалился — тест дальше не имеет смысла:
db, err := openDB(t)require.NoError(t, err) // не продолжать без БД
user, err := db.GetUser(1)assert.NoError(t, err) // продолжать, чтобы видеть остальные асертыassert.Equal(t, "alice", user.Name)4.15. Не “перетестируй” чужой код
Заголовок раздела «4.15. Не “перетестируй” чужой код»Не пиши тесты на json.Marshal или strings.Contains. Тестируй свой код.
4.16. Очищай testdata/fuzz от мусора
Заголовок раздела «4.16. Очищай testdata/fuzz от мусора»Когда fuzz нашёл краш — файл сохраняется в testdata/fuzz/FuzzXxx/. Это автоматически становится seed corpus. Коммить эти файлы — это регрессионные тесты.
4.17. Тесты должны падать осмысленно
Заголовок раздела «4.17. Тесты должны падать осмысленно»// плохоif got != want { t.Error("fail")}
// хорошоif got != want { t.Errorf("Add(%d, %d) = %d; want %d", a, b, got, want)}4.18. Не зависай в тестах
Заголовок раздела «4.18. Не зависай в тестах»Используй -timeout:
go test -timeout=30sИ контексты с дедлайнами:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»5.1. Чем t.Fatal отличается от t.Error?
Заголовок раздела «5.1. Чем t.Fatal отличается от t.Error?»t.Error помечает тест как failed, но продолжает выполнение функции. t.Fatal помечает + останавливает функцию через runtime.Goexit (т.е. defer вызовутся).
5.2. Что такое table-driven tests?
Заголовок раздела «5.2. Что такое table-driven tests?»Структура тестов: slice структур с входными данными и ожидаемыми результатами, цикл с t.Run. Делает тесты компактными, легко расширяемыми, видны причины фейлов.
5.3. Зачем t.Helper()?
Заголовок раздела «5.3. Зачем t.Helper()?»Чтобы сообщения об ошибках указывали на строку вызова helper-функции, а не на строку внутри неё. Помечает фрейм как пропускаемый при формировании traceback.
5.4. Что делает t.Parallel()?
Заголовок раздела «5.4. Что делает t.Parallel()?»Помечает тест как параллельный. Все параллельные тесты в пакете сначала “паркуются”, потом запускаются параллельно (с лимитом -parallel).
5.5. Что такое loop variable trap в subtests?
Заголовок раздела «5.5. Что такое loop variable trap в subtests?»До Go 1.22: переменная цикла была общая для всех итераций. В parallel-subtests это приводило к тому, что все горутины видели последнее значение. Лекарство: tt := tt внутри цикла. С Go 1.22 переменная — per-iteration scope, проблемы нет.
5.6. Чем TestMain отличается от t.Cleanup?
Заголовок раздела «5.6. Чем TestMain отличается от t.Cleanup?»TestMain(m *testing.M) — глобальный setup/teardown для всего пакета. t.Cleanup — callback при завершении конкретного теста (или subtest). Cleanup работает даже после t.Fatal.
5.7. Что такое testdata?
Заголовок раздела «5.7. Что такое testdata?»Специальная папка, содержимое которой Go-компилятор игнорирует. Туда складывают fixtures, golden files, fuzz corpus. Доступна тестам как обычный путь.
5.8. Как считается code coverage?
Заголовок раздела «5.8. Как считается code coverage?»go test -cover инструментирует код — добавляет счётчики в каждый basic block. После выполнения тестов посчитанные строки делятся на общее количество. Режимы: set, count, atomic.
5.9. Чем set отличается от atomic в -covermode?
Заголовок раздела «5.9. Чем set отличается от atomic в -covermode?»set— bool (выполнено/нет), не thread-safe, но дешёвый.atomic—int64с atomic ops, безопасен для параллельных тестов.count— int, но не thread-safe.
5.10. Как работает b.N в бенчмарках?
Заголовок раздела «5.10. Как работает b.N в бенчмарках?»Go итеративно подбирает b.N, пока общее время прогона не превысит -benchtime (по умолчанию 1s). Обычно растёт по схеме 1 → 100 → 10000 → … Цель — статистически значимое измерение.
5.11. Зачем b.ResetTimer()?
Заголовок раздела «5.11. Зачем b.ResetTimer()?»Чтобы исключить из измерения тяжёлый setup, выполненный до основного цикла. Также есть b.StopTimer()/b.StartTimer() для отрезков внутри цикла.
5.12. Что такое Example функции?
Заголовок раздела «5.12. Что такое Example функции?»Функции вида func ExampleXxx() { ... // Output: ... }. Go компилирует, запускает и сравнивает stdout с блоком // Output:. Документация, которая не врёт.
5.13. Что такое fuzz testing?
Заголовок раздела «5.13. Что такое fuzz testing?»Автоматическая генерация входных данных (мутации seed corpus, coverage-guided). Помогает находить edge cases. С Go 1.18 встроен: func FuzzXxx(f *testing.F), go test -fuzz=.
5.14. Чем httptest.NewServer отличается от httptest.NewRecorder?
Заголовок раздела «5.14. Чем httptest.NewServer отличается от httptest.NewRecorder?»NewServer— поднимает реальный HTTP-сервер на 127.0.0.1:. Для интеграционных тестов. NewRecorder—http.ResponseWriter, который записывает в память. Для unit-тестов хендлеров без сети.
5.15. Что такое race detector?
Заголовок раздела «5.15. Что такое race detector?»-race флаг включает ThreadSanitizer — динамическую проверку конкурентного доступа к памяти. Не находит все race condition (только в выполненном коде), но эффективно выявляет реальные баги. Замедляет 5-10x.
5.16. Можно ли вызывать t.Fatal из другой горутины?
Заголовок раздела «5.16. Можно ли вызывать t.Fatal из другой горутины?»Нет. t.Fatal/t.FailNow нужно вызывать только из той горутины, в которой запущен тест. Из других — panic. Используй каналы или sync для передачи ошибок.
5.17. Чем testify/assert отличается от testify/require?
Заголовок раздела «5.17. Чем testify/assert отличается от testify/require?»assert.*— fail продолжает тест (какt.Error).require.*— fail останавливает (какt.Fatal).
Полезно: require для setup’а, assert для основных проверок.
5.18. Когда использовать package foo_test вместо package foo в тестах?
Заголовок раздела «5.18. Когда использовать package foo_test вместо package foo в тестах?»package foo_test — внешний тест, доступен только публичный API. Хорошо для black-box тестирования, защищает от использования internal-функций. package foo — для white-box, тестирования приватных функций.
5.19. Что делает t.Cleanup() и чем отличается от defer?
Заголовок раздела «5.19. Что делает t.Cleanup() и чем отличается от defer?»t.Cleanup(fn) — регистрирует функцию, вызываемую при завершении теста или subtest. Преимущества:
- Работает в subtests (vs
deferв parent, который выполнится после всех subtests). - Выполнится при
t.Fatal(какdefer, черезruntime.Goexit). - Можно вызывать из helper’ов.
5.20. Что такое golden files?
Заголовок раздела «5.20. Что такое golden files?»Файлы с ожидаемым результатом, обычно в testdata/. Тесты сравнивают свой output с golden. Преимущество — легко обновлять (флаг -update), легко проверять глазами при изменениях.
5.21. Как протестировать функцию с временем/таймером?
Заголовок раздела «5.21. Как протестировать функцию с временем/таймером?»Варианты:
- Внедрить
time.Nowчерез интерфейс/функцию:clock Clock. - Использовать
testing/synctest(Go 1.24+, experimental) — фейковое время. - Использовать
time.Tickс короткими интервалами — не идеально.
5.22. Что такое testing.Short()?
Заголовок раздела «5.22. Что такое testing.Short()?»Возвращает true, если запущено go test -short. Позволяет пропустить долгие тесты:
if testing.Short() { t.Skip("...")}5.23. Как сравнить два бенчмарка?
Заголовок раздела «5.23. Как сравнить два бенчмарка?»go install golang.org/x/perf/cmd/benchstat@latestgo test -bench=. -count=10 > old.txt# changes...go test -bench=. -count=10 > new.txtbenchstat old.txt new.txtbenchstat показывает статистически значимые изменения, не “0.05ns тут лучше”.
5.24. Что произойдёт, если в Example есть // Output:, но реального вывода нет?
Заголовок раздела «5.24. Что произойдёт, если в Example есть // Output:, но реального вывода нет?»go test сравнит ожидаемый output с фактическим (пустым) и упадёт с сообщением о несовпадении. Пример обязательно должен производить ожидаемый stdout.
5.25. Как работает test caching?
Заголовок раздела «5.25. Как работает test caching?»Go кэширует результаты go test по хэшу: исходники + флаги + env. Если ничего не изменилось — возвращается (cached). Сбросить: go clean -testcache или go test -count=1.
6. Practice
Заголовок раздела «6. Practice»6.1. Базовый table-driven тест
Заголовок раздела «6.1. Базовый table-driven тест»Создай math/math.go:
package math
func Max(a, b int) int { if a > b { return a } return b}Напиши math_test.go с table-driven тестом, включающим:
- positive, negative, equal, zero
int.Max/int.Min
Запусти go test -v ./math.
6.2. Subtests с t.Parallel()
Заголовок раздела «6.2. Subtests с t.Parallel()»Возьми предыдущий тест, добавь t.Parallel() в каждый subtest. Запусти с -race:
go test -race -v ./math6.3. Helper функция
Заголовок раздела «6.3. Helper функция»Напиши:
func assertEqual[T comparable](t *testing.T, got, want T) { t.Helper() if got != want { t.Errorf("got %v; want %v", got, want) }}Используй в тестах. Замени t.Helper() на ничего и сравни сообщения.
6.4. TestMain setup/teardown
Заголовок раздела «6.4. TestMain setup/teardown»Напиши пакет, который использует БД (sqlite :memory:):
var db *sql.DB
func TestMain(m *testing.M) { var err error db, err = sql.Open("sqlite3", ":memory:") if err != nil { panic(err) }
// создать схему db.Exec("CREATE TABLE users (id INTEGER, name TEXT)")
code := m.Run() db.Close() os.Exit(code)}6.5. t.Cleanup
Заголовок раздела «6.5. t.Cleanup»Напиши тест с временным файлом:
func TestFile(t *testing.T) { f, err := os.CreateTemp("", "test-*.txt") require.NoError(t, err) t.Cleanup(func() { os.Remove(f.Name()) })
// ... используй f}Проверь, что файл удаляется даже при t.Fatal.
6.6. Coverage
Заголовок раздела «6.6. Coverage»Запусти:
go test -coverprofile=cov.out ./...go tool cover -html=cov.outИзучи HTML-отчёт. Найди непокрытые ветки, допиши тесты.
6.7. Бенчмарк
Заголовок раздела «6.7. Бенчмарк»func BenchmarkStrConcat(b *testing.B) { parts := []string{"a", "b", "c", "d", "e"}
b.Run("plus", func(b *testing.B) { for i := 0; i < b.N; i++ { s := "" for _, p := range parts { s += p } _ = s } })
b.Run("builder", func(b *testing.B) { for i := 0; i < b.N; i++ { var sb strings.Builder for _, p := range parts { sb.WriteString(p) } _ = sb.String() } })}Запусти go test -bench=. -benchmem. Сравни.
6.8. Example функция
Заголовок раздела «6.8. Example функция»func ExampleMax() { fmt.Println(Max(2, 5)) // Output: 5}Запусти go test -v -run Example. Поменяй 5 на 4 — увидь fail.
6.9. Fuzz test
Заголовок раздела «6.9. Fuzz test»func Reverse(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r)}
func FuzzReverse(f *testing.F) { f.Add("hello") f.Fuzz(func(t *testing.T, s string) { rev := Reverse(s) revRev := Reverse(rev) if s != revRev { t.Errorf("%q != %q", s, revRev) } })}Запусти:
go test -fuzz=FuzzReverse -fuzztime=30sЕсли использовать []byte вместо []rune — найдёт случай с UTF-8 (например, 世).
6.10. HTTP handler test
Заголовок раздела «6.10. HTTP handler test»func HelloHandler(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { name = "world" } fmt.Fprintf(w, "Hello, %s!", name)}
func TestHelloHandler(t *testing.T) { tests := []struct { name string url string want string }{ {"default", "/", "Hello, world!"}, {"named", "/?name=Alice", "Hello, Alice!"}, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest("GET", tt.url, nil) w := httptest.NewRecorder()
HelloHandler(w, req)
got := w.Body.String() if got != tt.want { t.Errorf("got %q; want %q", got, tt.want) } }) }}6.11. Golden file test
Заголовок раздела «6.11. Golden file test»var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) { got := Render(input) golden := filepath.Join("testdata", "render.golden")
if *update { if err := os.WriteFile(golden, got, 0644); err != nil { t.Fatal(err) } }
want, err := os.ReadFile(golden) if err != nil { t.Fatal(err) }
if !bytes.Equal(got, want) { t.Errorf("output mismatch:\n--- got\n%s\n--- want\n%s", got, want) }}6.12. Race condition
Заголовок раздела «6.12. Race condition»Намеренно сломанный код:
type Counter struct { n int}
func (c *Counter) Inc() { c.n++}
func TestCounter(t *testing.T) { c := &Counter{} var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() c.Inc() }() } wg.Wait()}go test -raceДолжен поймать DATA RACE. Исправь через sync/atomic или mutex.
7. Источники
Заголовок раздела «7. Источники»-
Официальная документация
testing— https://pkg.go.dev/testing Полный API reference. Самый авторитетный источник. -
The Go Blog: Using Subtests and Sub-benchmarks — https://go.dev/blog/subtests От Go team, объясняет
t.Runи парадигму subtests. -
The Go Blog: Fuzzing is Beta Ready (и follow-up) — https://go.dev/blog/fuzz-beta Введение в фаззинг. Обновляйся до новых статей по
synctest/fuzz. -
Go Wiki: Table Driven Tests — https://go.dev/wiki/TableDrivenTests Каноническая форма Go-тестов.
-
Russ Cox: Versions in Go + Дейв Чейни: testing tips — https://dave.cheney.net/category/testing Серия статей с лучшими практиками.
-
Effective Go: Testing — https://go.dev/doc/effective_go#testing Идиоматичные паттерны от Go team.
-
Дейв Чейни: Prefer table driven tests — https://dave.cheney.net/2019/05/07/prefer-table-driven-tests Глубокое обоснование табличного стиля.