Тестирование

Эта глава — одна из моих самых любимых, потому что именно здесь Go проявляет свою практичность. В отличие от многих других языков, где для тестирования нужно выбирать библиотеку (Jest, JUnit, PyTest), в Go инструмент тестирования встроен в сам язык и его стандартную консольную утилиту.

Написание программ — сложный процесс. Даже опытные разработчики допускают ошибки. В Go тестирование не является чем-то второстепенным; оно интегрировано в сам процесс разработки. Хорошие тесты — это страховка, которая позволяет вам менять код и быть уверенным, что вы ничего не сломали.

Основы тестирования

Чтобы написать тест в Go, нужно соблюдать три правила:

  1. Имя файла: Файл с тестами должен заканчиваться на _test.go.
  2. Имя функции: Функция должна начинаться со слова Test (например, TestAverage).
  3. Аргумент: Функция должна принимать один аргумент типа *testing.T.

Давайте создадим тест для нашего пакета math. В папке проекта math/ создайте файл math_test.go:

package math

import "testing"

func TestAverage(t *testing.T) {
    v := Average([]float64{1, 2})
    if v != 1.5 {
        t.Errorf("Ожидалось 1.5, получено %f", v)
    }
}

Запуск тестов

Откройте терминал в папке проекта и введите:

go test

Вы увидите сообщение PASS, если тест прошел успешно. Если вы хотите увидеть подробный отчет по каждой функции, используйте флаг «verbose»:

go test -v


Табличные тесты (Table-Driven Tests)

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

func TestAverageTable(t *testing.T) {
    // Определяем "таблицу" тестов
    tests := []struct {
        name     string
        values   []float64
        expected float64
    }{
        {"Два числа", []float64{1, 2}, 1.5},
        {"Одинаковые числа", []float64{1, 1, 1}, 1.0},
        {"Отрицательные", []float64{-1, 1}, 0.0},
        {"Пустой срез", []float64{}, 0.0},
    }

    for _, tt := range tests {
        // t.Run позволяет запускать каждый кейс как отдельный под-тест
        t.Run(tt.name, func(t *testing.T) {
            v := Average(tt.values)
            if v != tt.expected {
                t.Errorf("%s: ожидали %f, получили %f", tt.name, tt.expected, v)
            }
        })
    }
}

Почему это удобно?

  1. Легко добавлять кейсы: Просто добавьте новую строку в структуру.
  2. Читаемость: Весь сценарий тестирования виден в одном месте.
  3. Изоляция: Благодаря t.Run, если один кейс упадет, остальные продолжат выполняться.

Покрытие кода (Code Coverage)

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

Запустите команду:

go test -cover

Вы получите результат вроде coverage: 85.0% of statements.

Чтобы увидеть визуально, какие строки кода покрыты, а какие нет, используйте:

go test -coverprofile=cover.out
go tool cover -html=cover.out

Эта команда откроет браузер, где зеленым цветом будет подсвечен протестированный код, а красным — нет.


Задачи

  • Крайние случаи: Что вернет ваша функция Average, если передать ей nil или пустой срез []float64{}? Скорее всего, произойдет деление на ноль, что даст NaN. Исправьте функцию, чтобы она возвращала в таких случаях.
  • Min и Max: Напишите табличные тесты для функций Min и Max, которые вы создали в прошлой главе.
  • Документация через тесты: В Go есть понятие Example-тестов. Изучите, как работают функции, начинающиеся со слова Example.... Они отображаются в документации как примеры использования кода.
  • Покрытие: Добейтесь 100% покрытия для вашего пакета math.

Полезные ссылки

  1. Go by Example: Testing — быстрый справочник.
  2. Package testing (Official Doc) — документация стандартного пакета.
  3. Learn Go with Tests — великолепный курс по изучению Go через TDD (Test Driven Development).
  4. Table Driven Tests (Go Wiki) — подробнее о философии табличных тестов.