2. Naming conventions
Анатомия имен: Как называть тесты, чтобы вас понимали
В предыдущей статье 1. Организация тестов в проекте мы заложили фундамент архитектуры: определили границы пакетов, освоили теги компилятора //go:build и выделили каталоги testdata. Мы научились раскладывать компоненты по правильным ячейкам.
Но когда разработчик открывает файл *_test.go, он сталкивается с вопросом языка общения: как назвать тестовую функцию, локальную переменную или вложенный сабтест, чтобы коллега (или вы сами спустя полгода) мгновенно считал суть проверяемого сценария без необходимости построчно дебажить исходник?
В языке Go нет громоздких поведенческих фреймворков вроде RSpec (Ruby) или BDD-конструкций с многословными цепочками describe и it. Вся выразительность и строгость инструментария Go опираются на канонические соглашения об именовании (Naming Conventions). Следование этим соглашениям — верный признак зрелого Go-инженера, понимающего внутреннее устройство рантайма и утилиты go test.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph Files["1. Файловый уровень"]
F1["user.go (боевой код)"]
F2["user_test.go (Black-box: package user_test)"]
F3["user_internal_test.go (White-box: package user)"]
end
subgraph Funcs["2. Функциональный уровень"]
FN1["Test<Func> (TestParseURL)"]
FN2["Test<Type>_<Method> (TestUserRepo_GetByID)"]
FN3["Benchmark<Type>_<Method>"]:::process
FN4["Fuzz<Type>_<Method>"]:::process
FN5["Example<Type>_<Method>"]:::process
end
subgraph Runs["3. Табличный уровень (t.Run)"]
TR["tc.name: лаконичные фразы с пробелами<br/>'returns error when email is empty'"]:::error
end
subgraph Vars["4. Переменные утверждений"]
WG["want: ожидаемое значение<br/>got: фактический результат"]:::process
end
Files --> Funcs --> Runs --> Vars
1. Именование файлов: Зеркальное отражение
Базовое правило, заложенное в рантайм go build и go test: любые тестовые файлы обязаны иметь суффикс _test.go. Но в инженерной культуре принято строгое правило зеркального отражения (Mirroring).
Если бизнес-логика сосредоточена в файле user.go, её тесты должны располагаться исключительно в user_test.go. Аналогично для router.go тесты живут в router_test.go. Не превращайте проект в свалку, ссыпая сотни тестов из разных доменов в гигантский монолитный repository_test.go. Зеркальная раскладка позволяет мгновенно сопоставить реализацию и её спецификацию.
2. Именование функций: Строгий контракт
Утилита go test не использует сложную внешнюю конфигурацию: она сканирует AST-дерево скомпилированных файлов и с помощью рефлексии ищет функции, строго соответствующие сигнатуре: func TestXxx(*testing.T).
Здесь Xxx — любая непустая строка, начинающаяся с заглавной буквы (или символа юникода, не являющегося строчной буквой).
Тестирование обычных функций
Если вы тестируете чистую функцию ParseURL, тестовая функция должна называться зеркально: TestParseURL. Если тестируется функция Reverse, пишем TestReverse.
Тестирование методов структур (Method Convention)
В Go отсутствуют классы, но структуры наделяются методами через ресиверы. Если мы тестируем метод Save структуры UserRepository или метод GetByID структуры UserRepo, как отразить это в имени?
Общепринятый стандарт индустрии — использование символа подчёркивания _ для разграничения типа и метода:
// Тестируем метод GetByID у структуры UserRepo
func TestUserRepo_GetByID(t *testing.T) { ... }
// Тестируем метод Validate у структуры Order
func TestOrder_Validate(t *testing.T) { ... }
Такой формат невероятно удобен при запуске конкретных тестов из консоли:
go test -run=^TestUserRepo_ запустит тесты всех методов структуры UserRepo.
Для запуска конкретного метода можно передать полный паттерн: go test -v -run=^TestUserRepo_GetByID$ ./....
3. Именование Table-Driven сабтестов (t.Run)
В главе 4. Table driven tests мы использовали t.Run(tc.name, ...) для запуска сабтестов. Именование этих сценариев — это то, где многие ломают копья.
В поле tc.name не нужно писать искусственные конструкции вроде TestUserValidation_EmptyEmail, case1 или err_not_found. Используйте человекочитаемые предложения на английском языке с пробелами, описывающие ожидаемое поведение при конкретных входных условиях:
// Плохо:
name: "TestUserValidation_EmptyEmail"
name: "err_not_found"
name: "case1"
// Идиоматично и безупречно:
name: "successful creation of premium user"
name: "returns error when email is empty"
name: "conflict when user already exists"
4. Переменные want и got: Священная корова Go
В экосистемах Java (JUnit), C# (NUnit) или JavaScript (Jest) исторически укоренились длинные абстрактные термины expected (ожидаемое) и actual (фактическое):
// Java JUnit
assertEquals(expected, actual);
В культуре языка Go сложилась собственная лаконичная традиция, заложенная авторами стандартной библиотеки Кеном Томпсоном, Робом Пайком и Рассом Коксом. В Go повсеместно используются короткие, ясные англосаксонские слова:
want— то, что мы хотим получить согласно спецификации;got— то, что фактически вернул тестируемый код.
// НЕ ИДИОМАТИЧНО (с тяжелым акцентом Java/C#):
func TestCalculate(t *testing.T) {
expected := 42
actual := Calculate()
require.Equal(t, expected, actual)
}
// ИДИОМАТИЧНО (чистый стиль Go):
func TestCalculate(t *testing.T) {
want := 42
got := Calculate()
require.Equal(t, want, got)
}
Почему именно want и got?
- Компактность и выразительность: 4 буквы против 8 и 6. Код тестов избавляется от визуального шума.
-
Идеальное визуальное выравнивание: моноширинный шрифт делает сопоставление очевидным:
Text want: 42 got: 43 -
Стандарт в таблицах: в структуре
testCaseиспользуйтеwantиwantErr:
type testCase struct {
name string
input string
want int // вместо expected
wantErr bool // вместо expectsError
}
5. Именование Benchmarks, Fuzz и Examples
Правила именования для тестов производительности, фаззеров и живой документации следуют той же строгой логике префиксов.
- Benchmarks:
Benchmark<Type>_<Method>.
Пример:BenchmarkRouter_ServeHTTP(b *testing.B) - Fuzzing:
Fuzz<Type>_<Method>.
Пример:FuzzParseTLV(f *testing.F)
Особый случай: Примеры (Examples как документация)
В Go реализован элегантный механизм живых примеров: функции ExampleXxx(). Они одновременно служат автоматическими тестами, а утилита go doc и веб-портал pkg.go.dev автоматически связывают их с документацией вашей функции.
func ExampleParse()— привязывается к публичной функцииParse.func ExampleUser_Save()— привязывается к методуSaveструктурыUser.func ExampleUser_Save_withTimeout()— создаёт отдельную именованную вкладку примера (с суффиксомwithTimeout).
func ExampleReverse() {
fmt.Println(Reverse("hello"))
// Output: olleh
}
6. Именование тестовых двойников (Mocks/Stubs)
При проектировании подставных объектов (Test Doubles) следуйте единым префиксам, отражающим их ответственность:
Mock(например,MockUserRepository): умный объект, созданный вручную или через кодогенераторыgomock/mockery. Отслеживает факт вызова методов, число обращений и переданные аргументы, проверяя поведение вызывающей стороны.Stub(например,StubUserRepository): пассивная заглушка, возвращающая захардкоженные константные значения без инспекции входящих параметров.Fake(например,FakeUserRepositoryилиInMemoryUserRepository): полностью функциональная, но упрощённая реализация контракта (например, хранилище в оперативной памяти на базеsync.Mapвместо реальной СУБД).
Итог
- Файлы: строго отражают боевой код (
router_test.goдляrouter.go). Для приватных инвариантов используйте суффикс_internal_test.go. - Функции:
Test<FuncName>иTest<Type>_<Method>. Помните о регистре: строчная буква после словаTestприведёт к тихому пропуску теста. - Сабтесты: пишите лаконичные, понятные английские предложения с пробелами. Раннер сам преобразует их в читаемые пути.
- Переменные: навсегда откажитесь от
expected/actualв пользу идиоматичныхwantиgot. - Документация: используйте
ExampleXxxс комментарием// Output:— это одновременно тест и интерактивный пример в документации.
Теперь, когда код тестов говорит на чистом и понятном языке Go, мы готовы автоматизировать процесс их запуска в распределённых пайплайнах сборки. В следующей статье мы переходим к автоматизации: 3. Тестирование в CI CD.