Как писать unit-тесты в Go? Какие инструменты и подходы используются?

Ответ

Unit-тестирование — неотъемлемая часть разработки на Go, и язык предоставляет мощные встроенные инструменты для этого.

Основные инструменты и пакеты

  1. Пакет testing: Стандартная библиотека Go. Все тесты пишутся в файлах с суффиксом _test.go и запускаются командой go test.
  2. testify/assert и testify/require: Самая популярная сторонняя библиотека, которая предоставляет удобные функции для проверок (ассертов). assert продолжает выполнение теста после ошибки, а require немедленно его прерывает.
  3. testify/mock или gomock: Библиотеки для создания моков (mock objects) — заглушек, которые имитируют поведение внешних зависимостей (например, базы данных или API), позволяя изолировать тестируемый код.

Ключевые подходы и паттерны

  1. Table-Driven Tests (Табличные тесты): Это идиоматический подход в Go. Вместо написания отдельной функции для каждого тестового случая, создается срез структур, где каждая структура описывает один тест-кейс (входные данные, ожидаемый результат, название).

  2. Мокирование зависимостей: Для тестирования логики, зависящей от внешних систем, используются интерфейсы. В тестах реальная зависимость подменяется моком, который реализует тот же интерфейс и возвращает предопределенные данные.

  3. Тестирование на гонки данных (Race Condition): Go имеет встроенный детектор гонок. Запуск тестов с флагом go test -race помогает выявить проблемы конкурентного доступа к памяти, что критически важно для многопоточных приложений.

  4. Покрытие кода (Code Coverage): Команда go test -cover показывает, какой процент кода покрыт тестами. Можно сгенерировать детальный HTML-отчет: go test -coverprofile=coverage.out && go tool cover -html=coverage.out.

Пример: Табличный тест

Предположим, у нас есть простая функция Sum:

// file: calculator.go
package main

func Sum(a, b int) int {
    return a + b
}

Тест для нее будет выглядеть так:

// file: calculator_test.go
package main

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestSum(t *testing.T) {
    // Описание тестовых случаев
    testCases := []struct {
        name string // Название теста
        a    int    // Входной параметр 1
        b    int    // Входной параметр 2
        want int    // Ожидаемый результат
    }{
        {"Два положительных числа", 2, 3, 5},
        {"Положительное и отрицательное", 5, -3, 2},
        {"Два отрицательных числа", -1, -1, -2},
        {"С нулем", 10, 0, 10},
    }

    // Итерация по тестовым случаям
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            got := Sum(tc.a, tc.b)
            assert.Equal(t, tc.want, got, "результат должен совпадать с ожидаемым")
        })
    }
}

Ответ 18+ 🔞

Ага, смотри, вот тебе про тесты в Go, но без этой занудной академической хуйни. Сядь, слушай.

Так, значит, ты пишешь код на Go. И ты, конечно, не мудак, который потом руками всё проверяет. Ты пишешь тесты. В Go с этим всё просто, но есть свои приколы, которые надо знать, чтобы не обосраться.

Чем тут можно пользоваться, как инструментами

  1. testing — родной пакет. Это как родная хата, всё есть, но без особых удобств. Пишешь файлы с названием что-то_test.go, кричишь go test, и он всё прогоняет. Без этого нихуя.
  2. testify/assert и testify/require — твои лучшие друзья. Это уже не родная хата, а евроремонт. Вместо того чтобы писать if got != want { t.Errorf(...) }, ты просто орёшь assert.Equal(t, ожидаемое, полученное). Разница в том, что assert после ошибки продолжает тест, а require — сразу в сраку, прерывает всё. Выбирай по характеру.
  3. Моки (testify/mock или gomock). Это когда твой код общается с какой-то внешней хуйнёй — базой данных, другим сервисом. Чтобы не зависеть от того, жива ли эта хуйня, ты создаёшь её муляж — мок. И говоришь этому муляжу: «Слушай, когда к тебе обратятся, сделай вид, что ты база данных, и верни вот это». Идеально для изоляции.

Как не выстрелить себе в ногу: главные подходы

  1. Табличные тесты (Table-Driven Tests). Это святое, блядь. Не пиши ты десять функций TestSum_WithPositive, TestSum_WithNegative. Ты чё, совсем? Создаёшь один срез структур, где каждая строка — это отдельный тестовый случай: что на вход, что ожидаешь на выходе, и название, чтобы потом в логах не ебаться. Элегантно и сука эффективно.
  2. Мокирование. Твой код не должен падать, потому что упала база. Поэтому все зависимости выносишь в интерфейсы. А в тесте вместо реальной базы подсовываешь мок, который этот интерфейс реализует и делает только то, что тебе нужно для теста. Чистая изоляция, как в боксе.
  3. Гонки данных (Race Condition). А вот это, ёпта, самая хитрая жопа в многопоточности. Два потока лезут в одну переменную, и кто первый — хрен знает. Go тебе в помощь: запускаешь тесты с флагом -race, и он тебе подсветит, где у тебя эти опасные места. Обязательно делай.
  4. Покрытие кода (Code Coverage). Чтобы не было стыдно перед коллегами. go test -cover покажет процент. А если хочешь красивую картинку, где именно не покрыто, то go test -coverprofile=coverage.out && go tool cover -html=coverage.out — откроет в браузере, и ты увидишь, где ты сэкономил на тестах, пидарас.

Пример, чтобы было понятно, как это выглядит вживую

Допустим, у тебя есть функция сложения, проще некуда:

// calculator.go
package main

func Sum(a, b int) int {
    return a + b
}

Вот как для неё писать тест по-взрослому, с таблицей:

// calculator_test.go
package main

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestSum(t *testing.T) {
    // Вот она, мать её, таблица. Всё красиво и в одном месте.
    testCases := []struct {
        name string // Как обозвать случай, чтобы в логах не "TestSum/0" было
        a    int    // Первое слагаемое
        b    int    // Второе слагаемое
        want int    // Что, блядь, должно получиться
    }{
        {"Два плюса", 2, 3, 5},
        {"Плюс и минус", 5, -3, 2},
        {"Два минуса", -1, -1, -2},
        {"С нулём", 10, 0, 10},
    }

    // Пробегаемся по всей таблице
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) { // Запускаем подтест с вменяемым именем
            got := Sum(tc.a, tc.b) // Вызываем тестируемую функцию
            assert.Equal(t, tc.want, got, "А сумма-то не сошлась, ёпта!") // Проверяем
        })
    }
}

Вот и вся магия. Ничего сложного, но делать надо. Иначе потом будешь как тот Герасим — молча страдать, когда продакшн упадёт в три часа ночи.