Расскажите об основах тестирования в Go. Как писать юнит-тесты, табличные тесты и бенчмарки?

Ответ

Тестирование — это встроенная и первоклассная возможность языка Go, для которой используется стандартный пакет testing и утилита go test.

Основные правила:

  1. Тесты находятся в файлах с суффиксом _test.go.
  2. Тестовые файлы располагаются в том же пакете, что и тестируемый код.
  3. Тестовая функция должна начинаться с Test, принимать один аргумент t *testing.T и не возвращать значений.

1. Простой юнит-тест:

// В файле sum.go
func Sum(a, b int) int {
    return a + b
}

// В файле sum_test.go
func TestSum(t *testing.T) {
    got := Sum(2, 3)
    want := 5
    if got != want {
        t.Errorf("Sum(2, 3) = %d; want %d", got, want)
    }
}

2. Табличные тесты (Table-Driven Tests): Это идиоматичный и предпочтительный способ для проверки множества кейсов. Для изоляции тестов и более четкого вывода используется t.Run().

func TestSumTable(t *testing.T) {
    tests := []struct {
        name string // Имя под-теста
        a    int
        b    int
        want int
    }{
        {"positive numbers", 1, 2, 3},
        {"zeroes", 0, 0, 0},
        {"negative and positive", -1, 1, 0},
        {"negative numbers", -2, -3, -5},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Sum(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Sum(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

3. Бенчмарки (Benchmarks): Для измерения производительности. Функция должна начинаться с Benchmark и принимать *testing.B.

func BenchmarkSum(b *testing.B) {
    // b.N - это количество итераций, которое определяет сам фреймворк
    for i := 0; i < b.N; i++ {
        Sum(100, 200)
    }
}

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

  • go test: запуск всех тестов в текущем пакете.
  • go test -v: запуск с подробным выводом, включая имена запущенных тестов.
  • go test ./...: запуск тестов во всех под-пакетах.
  • go test -run TestSumTable/zeroes: запуск конкретного под-теста.
  • go test -cover: показать покрытие кода тестами.
  • go test -bench .: запуск всех бенчмарков.

Ответ 18+ 🔞

А, тестирование в Go! Ну что ж, дружище, сейчас я тебе так разложу эту хуйню по полочкам, что ты сам захочешь тесты писать, даже если обычно ты их как чёрт ладана боишься.

Смотри, в Go тестирование — это не какая-то обуза, которую прикрутили сверху, а прямо в языке зашито, на уровне пизды. Всё через стандартный пакет testing и команду go test. Красота, а не жизнь!

Главные правила, которые надо запомнить, как «отче наш»:

  1. Файлы с тестами называй так: сумма_test.go. Видишь _test? Вот это наш маркер, наш маячок.
  2. Кидай эти файлы в тот же самый пакет, где и основной код лежит. Не выёбывайся.
  3. Сама тестовая функция должна орать на всех углах, что она тест: начинаться с Test, принимать одну штуку t *testing.T и нихуя не возвращать. Вообще.

1. Ну, самый простой тест, для разминки:

// В файле sum.go
func Sum(a, b int) int {
    return a + b
}

// В файле sum_test.go
func TestSum(t *testing.T) {
    got := Sum(2, 3) // Что получили
    want := 5        // Чего ждали
    if got != want { // А если не сошлось?
        t.Errorf("Sum(2, 3) = %d; want %d", got, want) // То орем на всю деревню
    }
}

Вот и всё, ёпта! Запустил go test — и сиди, смотри, как зелёные строчки бегут. Или красные, если накосячил.

2. Теперь главная фишка — табличные тесты (Table-Driven Tests). Это, блядь, святое! Все так делают. Вместо того чтобы десять функций городить, ты пишешь одну, но туда запихиваешь овердохуища разных случаев.

func TestSumTable(t *testing.T) {
    tests := []struct {
        name string // Имя подтеста, чтоб в отчёте понятно было, что сломалось
        a    int
        b    int
        want int
    }{
        {"положительные числа", 1, 2, 3},
        {"нули", 0, 0, 0},
        {"минус и плюс", -1, 1, 0},
        {"оба отрицательные", -2, -3, -5},
    }

    for _, tt := range tests { // Бежим по всем случаям
        t.Run(tt.name, func(t *testing.T) { // Запускаем каждый как отдельный подтест
            got := Sum(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Sum(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

Вот это уже мощь! t.Run() — это чтобы если один кейс ебнулся, остальные всё равно побежали, и ты видел, где конкретно пиздец, а не гадал на кофейной гуще.

3. Бенчмарки, или «а не тормозит ли моя функция, как говно в проруби?» Тут функция должна кричать Benchmark и хватать *testing.B.

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

Как это всё запускать, чтобы не опозориться:

  • go test — всё в текущем пакете пошло-поехало.
  • go test -v — подробненько так, с именами тестов. Люблю это дело.
  • go test ./... — война на всех фронтах, по всем подпакетам.
  • go test -run TestSumTable/нули — запустить только тест для нулей. Удобно, когда дебажишь.
  • go test -cover — посмотреть, какой процент кода ты своими тестами прикрыл. Стыдно, если мало.
  • go test -bench . — погнали бенчмарки! Смотрим, не сосёт ли наша функция.

Вот и вся наука. Не так страшен чёрт, как его малюют. Главное — начать, а там, глядишь, и понравится, когда после рефакторинга все тесты зелёные и ты спокоен, как удав.