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, как отразить это в имени?

Общепринятый стандарт индустрии — использование символа подчёркивания _ для разграничения типа и метода:

Go
// Тестируем метод 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. Используйте человекочитаемые предложения на английском языке с пробелами, описывающие ожидаемое поведение при конкретных входных условиях:

Go
// Плохо:
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
// Java JUnit
assertEquals(expected, actual);

В культуре языка Go сложилась собственная лаконичная традиция, заложенная авторами стандартной библиотеки Кеном Томпсоном, Робом Пайком и Рассом Коксом. В Go повсеместно используются короткие, ясные англосаксонские слова:

  • want — то, что мы хотим получить согласно спецификации;
  • got — то, что фактически вернул тестируемый код.
Go
// НЕ ИДИОМАТИЧНО (с тяжелым акцентом 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?

  1. Компактность и выразительность: 4 буквы против 8 и 6. Код тестов избавляется от визуального шума.
  2. Идеальное визуальное выравнивание: моноширинный шрифт делает сопоставление очевидным:

    Text
    want: 42
    got:  43
    
  3. Стандарт в таблицах: в структуре testCase используйте want и wantErr:

Go
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).
Go
func ExampleReverse() {
    fmt.Println(Reverse("hello"))
    // Output: olleh
}

6. Именование тестовых двойников (Mocks/Stubs)

При проектировании подставных объектов (Test Doubles) следуйте единым префиксам, отражающим их ответственность:

  • Mock (например, MockUserRepository): умный объект, созданный вручную или через кодогенераторы gomock / mockery. Отслеживает факт вызова методов, число обращений и переданные аргументы, проверяя поведение вызывающей стороны.
  • Stub (например, StubUserRepository): пассивная заглушка, возвращающая захардкоженные константные значения без инспекции входящих параметров.
  • Fake (например, FakeUserRepository или InMemoryUserRepository): полностью функциональная, но упрощённая реализация контракта (например, хранилище в оперативной памяти на базе sync.Map вместо реальной СУБД).

Итог

  1. Файлы: строго отражают боевой код (router_test.go для router.go). Для приватных инвариантов используйте суффикс _internal_test.go.
  2. Функции: Test<FuncName> и Test<Type>_<Method>. Помните о регистре: строчная буква после слова Test приведёт к тихому пропуску теста.
  3. Сабтесты: пишите лаконичные, понятные английские предложения с пробелами. Раннер сам преобразует их в читаемые пути.
  4. Переменные: навсегда откажитесь от expected/actual в пользу идиоматичных want и got.
  5. Документация: используйте ExampleXxx с комментарием // Output: — это одновременно тест и интерактивный пример в документации.

Теперь, когда код тестов говорит на чистом и понятном языке Go, мы готовы автоматизировать процесс их запуска в распределённых пайплайнах сборки. В следующей статье мы переходим к автоматизации: 3. Тестирование в CI CD.