🐹 Go

Code-Testen in Go (Golang): Eine ausführliche Anleitung

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты написал программу для калькулятора. Ты уверен, что она работает идеально. Но вдруг, когда ты вводишь "2 + 2", программа выдает "5". Знакомая ситуация? Чтобы такого не происходило, программисты придумали тестирование кода.

Давай проведем аналогию. Представь, что ты — шеф-повар в ресторане. Ты приготовил новое блюдо. Прежде чем подать его посетителю, ты обязательно попробуешь его сам. Ты проверяешь: достаточно ли соли? Не пережарено ли мясо? Правильно ли подобраны специи? Это и есть тестирование.

В программировании всё то же самое. Ты пишешь код, а потом проверяешь, делает ли он то, что задумано. Тестирование — это не просто "запустить программу и посмотреть". Это целая наука, которая помогает:

  • Находить ошибки до того, как их увидят пользователи
  • Убеждаться, что после изменений в коде всё продолжает работать
  • Экономить время — лучше потратить час на тесты, чем неделю на поиск бага
  • Повышать качество программы
Важно: Хороший программист отличается от плохого не тем, что не делает ошибок, а тем, что умеет их находить и исправлять до того, как они навредят пользователям.

В языке Go тестирование — это не дополнительная опция, а часть культуры программирования. В Go есть встроенные инструменты для тестирования, которые делают этот процесс простым и удобным.

Теория: как это работает изнутри

Давай разберемся, как устроено тестирование в Go "под капотом".

Основные виды тестов

В программировании есть несколько уровней тестирования. Представь, что ты проверяешь автомобиль:

  • Модульные тесты (Unit-тесты) — проверка отдельных деталей: работает ли двигатель? Крутятся ли колеса? В программировании это проверка отдельных функций или методов.
  • Интеграционные тесты — проверка, как детали работают вместе: передает ли двигатель крутящий момент на колеса? В программировании — как взаимодействуют разные части программы.
  • Функциональные тесты — проверка, выполняет ли автомобиль свою функцию: может ли он ехать? В программировании — работает ли программа так, как задумано.

В этой статье мы сосредоточимся на модульных тестах, потому что это основа основ.

Как Go запускает тесты

В Go есть специальная команда go test. Когда ты её запускаешь, происходит следующее:

  1. Go ищет все файлы, которые заканчиваются на _test.go
  2. В этих файлах он ищет функции, которые начинаются с Test
  3. Запускает каждую такую функцию
  4. Собирает результаты и показывает, какие тесты прошли, а какие — нет

Правила именования

Чтобы Go понял, что файл или функция — это тест, нужно соблюдать строгие правила:

Элемент Правило Пример
Имя файла Должно заканчиваться на _test.go calculator_test.go
Имя функции Должно начинаться с Test TestAdd
Параметры функции Должна принимать *testing.T func TestAdd(t *testing.T)
Запомни: Если ты нарушишь хотя бы одно правило, Go просто не увидит твой тест. Он не выдаст ошибку — он просто проигнорирует твой файл.

Синтаксис в Go (Golang): все формы и варианты

Теперь давай разберем, как писать тесты в Go. У нас есть несколько инструментов:

1. Базовый тест с t.Error и t.Fatal

Самый простой способ — использовать методы объекта t:

  • t.Error() — сообщает об ошибке, но продолжает выполнение теста
  • t.Fatal() — сообщает об ошибке и немедленно останавливает тест

2. Табличные тесты (Table-driven tests)

Это фирменный стиль Go. Вместо того чтобы писать отдельный тест для каждого случая, ты создаешь таблицу с входными данными и ожидаемыми результатами, а затем запускаешь один цикл, который проверяет все случаи.

3. Тесты с примерами (Example tests)

Go позволяет писать тесты, которые одновременно служат документацией. Они показывают, как использовать функцию, и автоматически проверяют, что вывод совпадает с ожидаемым.

4. Бенчмарки (Benchmark tests)

Это тесты производительности. Они измеряют, сколько времени выполняется функция. Название функции должно начинаться с Benchmark.

Примеры кода — от простого к сложному

Давай напишем программу-калькулятор и протестируем её. Сначала создадим основной файл calculator.go:

// Файл: calculator.go
// Пакет calculator содержит функции для простых математических операций
package calculator

// Add складывает два числа и возвращает результат
func Add(a, b int) int {
    return a + b
}

// Subtract вычитает второе число из первого
func Subtract(a, b int) int {
    return a - b
}

// Multiply умножает два числа
func Multiply(a, b int) int {
    return a * b
}

// Divide делит первое число на второе
// Возвращает результат и ошибку (если деление на ноль)
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("деление на ноль")
    }
    return a / b, nil
}

Пример 1: Простейший тест

// Файл: calculator_test.go
package calculator

import "testing"

// TestAdd проверяет функцию сложения
func TestAdd(t *testing.T) {
    // Вызываем функцию, которую хотим проверить
    result := Add(2, 3)
    
    // Проверяем, что результат равен ожидаемому
    if result != 5 {
        // Если не совпало — сообщаем об ошибке
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

Объяснение:

  • package calculator — тест должен быть в том же пакете, что и тестируемый код
  • import "testing" — подключаем пакет для тестирования
  • func TestAdd(t *testing.T) — функция теста, начинается с Test, принимает указатель на testing.T
  • t.Errorf — сообщает об ошибке, но продолжает выполнение

Пример 2: Тест с несколькими проверками

// TestMultiple проверяет несколько случаев в одном тесте
func TestMultiple(t *testing.T) {
    // Проверяем сложение
    if Add(1, 1) != 2 {
        t.Error("1 + 1 должно быть 2")
    }
    
    // Проверяем вычитание
    if Subtract(5, 3) != 2 {
        t.Error("5 - 3 должно быть 2")
    }
    
    // Проверяем умножение
    if Multiply(4, 3) != 12 {
        t.Error("4 * 3 должно быть 12")
    }
}

Объяснение: В одном тесте можно проверять несколько функций. Но это не очень хорошая практика — лучше каждый тест проверяет одну вещь.

Пример 3: Табличный тест (Table-driven test)

// TestAddTable использует табличный подход для тестирования сложения
func TestAddTable(t *testing.T) {
    // Создаем структуру для хранения тестовых данных
    // Каждый элемент — это один тестовый случай
    tests := []struct {
        name string      // Имя теста (для понятных сообщений)
        a, b int         // Входные данные
        want int         // Ожидаемый результат
    }{
        {"положительные числа", 2, 3, 5},
        {"отрицательные числа", -1, -2, -3},
        {"ноль", 0, 5, 5},
        {"большие числа", 1000, 2000, 3000},
    }
    
    // Проходим по всем тестовым случаям
    for _, tt := range tests {
        // Запускаем подтест с именем tt.name
        t.Run(tt.name, func(t *testing.T) {
            // Вызываем функцию и проверяем результат
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

Объяснение:

  • Мы создаем срез структур, где каждая структура — один тестовый случай
  • t.Run() запускает подтест с именем — это удобно, когда один тест падает, остальные продолжают работать
  • Такой подход легко расширять: просто добавляешь новую строку в таблицу

Пример 4: Тест с проверкой ошибок

// TestDivide проверяет функцию деления, включая обработку ошибок
func TestDivide(t *testing.T) {
    // Тест 1: обычное деление
    result, err := Divide(10, 2)
    if err != nil {
        t.Errorf("Divide(10, 2) вернула ошибку: %v", err)
    }
    if result != 5 {
        t.Errorf("Divide(10, 2) = %d; want 5", result)
    }
    
    // Тест 2: деление на ноль
    _, err = Divide(10, 0)
    if err == nil {
        t.Error("Divide(10, 0) должна вернуть ошибку, но не вернула")
    }
}

Объяснение: Когда функция возвращает ошибку, мы должны проверить и результат, и ошибку. В случае деления на ноль мы ожидаем ошибку — если её нет, тест должен упасть.

Пример 5: Тест с подготовкой и очисткой (Setup и Teardown)

// TestWithSetup демонстрирует, как подготовить данные перед тестом
func TestWithSetup(t *testing.T) {
    // Подготовка данных (Setup)
    // В реальном проекте здесь может быть подключение к БД или создание файла
    testData := []int{1, 2, 3, 4, 5}
    
    // Выполняем тест
    sum := 0
    for _, v := range testData {
        sum = Add(sum, v)
    }
    
    // Проверяем результат
    if sum != 15 {
        t.Errorf("Сумма чисел = %d; want 15", sum)
    }
    
    // Очистка (Teardown) происходит автоматически при выходе из функции
    // В реальном проекте здесь может быть закрытие соединения или удаление файла
}

Объяснение: Иногда перед тестом нужно подготовить данные (например, создать временный файл), а после теста — очистить. В Go это делается прямо в функции теста.

Пример 6: Тест-пример (Example test)

// ExampleAdd — это тест-пример, который служит документацией
// Он показывает, как использовать функцию Add
func ExampleAdd() {
    sum := Add(3, 4)
    fmt.Println(sum)
    // Output: 7
}

Объяснение:

  • Функция начинается с Example, а не Test
  • В конце обязательно пишем // Output: ожидаемый результат
  • Go запускает эту функцию и проверяет, что вывод совпадает с комментарием
  • Такие тесты автоматически включаются в документацию на pkg.go.dev

Пример 7: Бенчмарк (тест производительности)

// BenchmarkAdd измеряет производительность функции Add
func BenchmarkAdd(b *testing.B) {
    // b.N — это количество итераций, которое Go подбирает автоматически
    for i := 0; i < b.N; i++ {
        Add(100, 200)
    }
}

Объяснение:

  • Функция начинается с Benchmark и принимает *testing.B
  • Цикл for i := 0; i < b.N; i++ — обязателен, Go сам решает, сколько раз запустить
  • Запускается командой: go test -bench=.

Пример 8: Пропуск теста (Skip)

// TestSkipped демонстрирует, как пропустить тест при определенных условиях
func TestSkipped(t *testing.T) {
    // Если мы на Windows, пропускаем этот тест
    if runtime.GOOS == "windows" {
        t.Skip("Этот тест не работает на Windows")
    }
    
    // Если мы дошли сюда, выполняем тест
    result := Add(1, 1)
    if result != 2 {
        t.Errorf("Add(1, 1) = %d; want 2", result)
    }
}

Объяснение: Иногда тест нужно пропустить (например, если он работает только на Linux). t.Skip() останавливает тест и помечает его как пропущенный.

Сравнение с Python: чем похоже и чем отличается

Если ты знаком с Python, тебе будет легче понять тестирование в Go. Давай сравним:

Характеристика Go Python
Название тестового файла имя_test.go test_имя.py
Название тестовой функции TestИмя(t *testing.T) def test_имя():
Запуск тестов go test pytest или python -m unittest
Проверка равенства if got != want { t.Error(...) } assertEqual(got, want)
Табличные тесты Встроенная идиома (срезы структур) Через @pytest.mark.parametrize
Бенчмарки Встроенные (BenchmarkИмя) Через сторонние библиотеки
Примеры-тесты Встроенные (ExampleИмя) Через doctest
Фикстуры (Setup/Teardown) Вручную в функции теста setUp() и tearDown()

Главные отличия:

  1. Явность vs магия: В Go всё явно. Ты сам пишешь условие и вызываешь t.Error. В Python есть много "магии" — assert сам выбрасывает исключение.
  2. Скорость: Тесты в Go работают намного быстрее, потому что Go — компилируемый язык.
  3. Простота: В Go нет сложных фреймворков для тестирования — всё уже встроено в стандартную библиотеку.
Совет: Если ты переходишь с Python на Go, не пытайся искать аналоги pytest или unittest. Просто пиши тесты так, как принято в Go — с таблицами и явными проверками.

Типичные ошибки и подводные камни

Даже опытные программисты иногда допускают ошибки в тестах. Вот самые распространенные:

Ошибка 1: Тест ничего не проверяет

// ❌ ПЛОХОЙ пример: тест, который всегда проходит
func TestBad(t *testing.T) {
    Add(1, 2) // Вызвали функцию, но не проверили результат
    // Тест пройдет, даже если Add сломана
}

Решение: Всегда проверяй результат. Если нечего проверять — удали тест.

Ошибка 2: Использование t.Fatal вместо t.Error

// ❌ ПЛОХОЙ пример: останавливает тест при первой ошибке
func TestBadFatal(t *testing.T) {
    if Add(1, 1) != 2 {
        t.Fatal("Ошибка в сложении") // Тест остановится здесь
    }
    if Add(2, 2) != 4 {
        t.Fatal("Ошибка в сложении") // Этот код никогда не выполнится
    }
}

Решение: Используй t.Error, если хочешь увидеть все ошибки. t.Fatal используй только когда продолжение теста невозможно (например, не удалось подключиться к базе данных).

Ошибка 3: Зависимость тестов друг от друга

// ❌ ПЛОХОЙ пример: тест зависит от другого теста
var globalCounter int

func TestFirst(t *testing.T) {
    globalCounter = 10 // Устанавливаем глобальную переменную
}

func TestSecond(t *testing.T) {
    // Этот тест ожидает, что TestFirst уже выполнился
    if globalCounter != 10 {
        t.Error("Ожидалось 10")
    }
}

Решение: Каждый тест должен быть независимым. Не используй глобальные переменные для передачи данных между тестами.

Ошибка 4: Тестирование слишком многого в одном тесте

// ❌ ПЛОХОЙ пример: один тест проверяет всё
func TestEverything(t *testing.T) {
    // Проверяем сложение
    if Add(1, 1) != 2 { t.Error("Add failed") }
    // Проверяем вычитание
    if Subtract(5, 3) != 2 { t.Error("Subtract failed") }
    // Проверяем умножение
    if Multiply(4, 3) != 12 { t.Error("Multiply failed") }
    // Проверяем деление
    if result, _ := Divide(10, 2); result != 5 { t.Error("Divide failed") }
}

Решение: Каждый тест должен проверять одну вещь. Если тест упал, ты должен сразу понять, что именно сломалось.

Ошибка 5: Игнорирование ошибок в тестах

// ❌ ПЛОХОЙ пример: игнорируем ошибку
func TestBadIgnore(t *testing.T) {
    result, _ := Divide(10, 0) // Игнорируем ошибку!
    // result будет 0, но мы не знаем, почему
    if result != 0 {
        t.Error("Ожидалось 0")
    }
}

Решение: Всегда проверяй ошибки. Если функция может вернуть ошибку — проверь её.

Лучшие практики и советы профессионалов

Вот что советуют опытные Go-разработчики:

1. Следуй правилу "Один тест — одна проверка"

Каждый тест должен проверять одно поведение. Если тест называется TestAddPositiveNumbers, он должен проверять только сложение положительных чисел.

2. Используй осмысленные имена

Название теста должно описывать, что он проверяет:

// ✅ Хорошие имена
func TestAddPositiveNumbers(t *testing.T) {}
func TestAddNegativeNumbers(t *testing.T) {}
func TestAddWithZero(t *testing.T) {}

// ❌ Плохие имена
func TestAdd1(t *testing.T) {}
func TestCheck(t *testing.T) {}

3. Пиши тесты до кода (TDD — Test-Driven Development)

Это подход, при котором ты сначала пишешь тест, а потом — код, который этот тест проходит. Преимущества:

  • Ты точно знаешь, что код работает
  • Код получается более простым и понятным
  • Ты не пишешь лишнего кода

4. Используй табличные тесты для множества случаев

Если функция должна работать с разными входными данными — используй таблицу. Это делает тесты читаемыми и легко расширяемыми.

5. Запускай тесты часто

Не жди, пока напишешь всю программу. Запускай тесты после каждого небольшого изменения. В Go это делается одной командой:

go test ./...  # Запускает все тесты во всех пакетах

6. Следи за покрытием кода (Code Coverage)

Покрытие показывает, сколько процентов твоего кода выполняется во время тестов. Запускается так:

go test -cover

Стремись к покрытию 80% и выше, но помни: 100% покрытие не гарантирует отсутствие ошибок.

7. Не тестируй чужой код

Не нужно писать тесты для стандартной библиотеки Go или сторонних библиотек. Тестируй только свой код.

Вопросы для самопроверки

  1. Вопрос: Какое расширение должны иметь файлы с тестами в Go?

    Ответ: _test.go. Например, calculator_test.go.

  2. Вопрос: С какой буквы должна начинаться функция теста?

    Ответ: С большой буквы T — Test. Например, TestAdd.

  3. Вопрос: В чем разница между t.Error и t.Fatal?

    Ответ: t.Error сообщает об ошибке и продолжает выполнение теста. t.Fatal сообщает об ошибке и немедленно останавливает тест.

  4. Вопрос: Что такое табличный тест и зачем он нужен?

    Ответ: Это подход, при котором ты создаешь таблицу с входными данными и ожидаемыми результатами, а затем запускаешь один цикл для проверки всех случаев. Это делает тесты читаемыми и легко расширяемыми.

  5. Вопрос: Как запустить все тесты в проекте?

    Ответ: Командой go test ./... (три точки означают "все пакеты").

  6. Вопрос: Что такое бенчмарк и как его запустить?

    Ответ: Бенчмарк — это тест производительности. Функция начинается с Benchmark и принимает *testing.B. Запускается командой go test -bench=.

  7. Вопрос: Можно ли пропустить тест при определенных условиях?

    Ответ: Да, с помощью t.Skip(). Например, если тест работает только на Linux, можно пропустить его на Windows.

  8. Вопрос: Что такое покрытие кода (code coverage)?

    Ответ: Это метрика, показывающая, сколько процентов кода выполняется во время тестов. Запускается командой go test -cover.

  9. Вопрос: В чем отличие тестов в Go от тестов в Python?

    Ответ: В Go тесты пишутся явно (сам проверяешь условия и вызываешь t.Error), а в Python часто используется магия assert. Также в Go тесты работают быстрее, и встроена поддержка бенчмарков.

  10. Вопрос: Что такое TDD (Test-Driven Development)?

    Ответ: Это подход, при котором сначала пишется тест, а потом — код, который этот тест проходит. Это помогает писать более качественный код и избегать лишней работы.

Итог и что изучить дальше

Мы прошли большой путь! Ты узнал:

  • Что такое тестирование и зачем оно нужно
  • Как устроены тесты в Go "под капотом"
  • Все формы тестов: базовые, табличные, примеры, бенчмарки
  • Как писать тесты на практике — от простых до сложных
  • Чем тестирование в Go отличается от Python
  • Какие ошибки чаще всего допускают новички
  • Лучшие практики от профессионалов

Теперь ты готов писать тесты для своих Go-программ. Но это только начало! Вот что стоит изучить дальше:

  1. Моки (Mocks) и стабы (Stubs) — как тестировать код, который работает с базой данных или внешними API
  2. Интеграционные тесты — как тестировать взаимодействие между разными частями программы
  3. Тестирование HTTP-серверов — как тестировать веб-приложения на Go
  4. Пакет testing/iotest — утилиты для тестирования ввода-вывода
  5. Пакет testing/quick — генерация случайных тестовых данных
Главный совет: Не бойся писать тесты. Даже если сначала они кажутся сложными и лишними — со временем ты поймешь, что это лучшая инвестиция времени в программировании. Тесты спасают от бессонных ночей в поисках багов и делают твой код надежным, как швейцарские часы.

Помни: хороший программист — это не тот, кто не делает ошибок, а тот, кто умеет их находить и исправлять. Тестирование — твой главный инструмент в этом деле. Удачи в изучении Go!