Что такое юнит-тесты и зачем они нужны?

Ответ

Юнит-тесты (модульные тесты) — это тип автоматизированных тестов, которые проверяют корректность работы наименьших изолированных частей программы, таких как функции или методы.

Основные цели юнит-тестов:

  1. Обнаружение ошибок на ранней стадии: Позволяют быстро найти и исправить баги в новой или измененной логике.
  2. Безопасный рефакторинг: Если после изменения кода тесты по-прежнему проходят, вы можете быть уверены, что не сломали существующую функциональность.
  3. Живая документация: Тесты наглядно демонстрируют, как должен работать код и какие крайние случаи (edge cases) он обрабатывает.
  4. Улучшение дизайна кода: Необходимость писать тестируемый код подталкивает к созданию более слабосвязанных и модульных компонентов.

Пример в Go:

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

Код для тестирования:

// file: sum.go
func Sum(a, b int) int {
    return a + b
}

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

// file: sum_test.go
package main

import "testing"

func TestSum(t *testing.T) {
    result := Sum(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Sum(2, 3) = %d; ожидалось %d", result, expected)
    }
}

Лучшая практика в Go: Table-Driven Tests

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

// file: sum_test.go
func TestSumTable(t *testing.T) {
    cases := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"Положительные числа", 2, 3, 5},
        {"Отрицательные числа", -2, -3, -5},
        {"Смешанные числа", 2, -3, -1},
        {"С нулем", 5, 0, 5},
    }

    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            result := Sum(tc.a, tc.b)
            if result != tc.expected {
                t.Errorf("Sum(%d, %d) = %d; ожидалось %d", tc.a, tc.b, result, tc.expected)
            }
        })
    }
}

Запуск тестов выполняется командой: go test.

Ответ 18+ 🔞

А, юнит-тесты! Ну это ж, блядь, как та самая Муму, только наоборот — не топить, а спасать от утопления в говнокоде, понимаешь?

Смотри, вот представь: пишешь ты функцию, вроде всё работает, а потом через месяц приходит какой-нибудь полупидор, типа меня, и начинает её менять. И тут, сука, начинается — он что-то там подкрутил, и всё, пиздец, вся логика рассыпалась как карточный домик от чиха. А юнит-тесты — это как раз те самые охранники, которые кричат: «Э, бошка думай! Ты тут нахуярил, а тут сломалось!».

Зачем они вообще нужны, эти тесты, ёпта?

  1. Ловить косяки сразу. Написал функцию — тут же её и протестировал. Не жди, пока пользователь начнёт материться, что у него всё ебнулось.
  2. Рефакторить без страха. Хочешь переписать код красиво? Впердоливай! Если тесты зелёные — значит, не сломал старую логику. Если красные — ну, сам понимаешь, иди чини.
  3. Они как документация, только живая. Открыл тесты и сразу видишь, как эту функцию кормить и что от неё ждать. Лучше любой писанины в комментариях.
  4. Заставляют писать нормальный код. Потому что протестировать спагетти, где всё намешано в одну кучу — это пиздец, волнение ебать. Приходится делать модули изолированными.

Ну и пример на Go, чтоб не быть пиздаболом.

Вот у нас есть функция, проще некуда. Складывает два числа, в рот меня чих-пых.

Сам код, святое:

// sum.go
func Sum(a, b int) int {
    return a + b
}

А вот её тест. В Go для этого есть свой встроенный инструмент, не надо никаких овердохуищных фреймворков.

Тестик, мать его:

// sum_test.go
package main

import "testing"

func TestSum(t *testing.T) {
    result := Sum(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Sum(2, 3) = %d; ожидалось %d", result, expected)
    }
}

Запускаешь go test, и если всё ок — тишина. А если накосячил — он тебе такую оплеуху в виде ошибки выдаст, мало не покажется.

Но настоящие мужики в Go пишут табличные тесты!

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

// sum_test.go
func TestSumTable(t *testing.T) {
    // Вот наша таблица с кейсами. Каждый — отдельная история.
    cases := []struct {
        name     string // Какой случай проверяем
        a, b     int    // Что даём на вход
        expected int    // Что ждём на выходе
    }{
        {"Обычные плюсовые", 2, 3, 5},
        {"Оба минусовые", -2, -3, -5},
        {"Плюс и минус", 2, -3, -1},
        {"С нулём", 5, 0, 5},
        // Хочешь добавить ещё случай? Просто пиши сюда новую строчку!
    }

    // И поехали проверять каждый кейс по очереди
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) { // t.Run запускает подтест с именем
            result := Sum(tc.a, tc.b)
            if result != tc.expected {
                t.Errorf("Sum(%d, %d) = %d; ожидалось %d", tc.a, tc.b, result, tc.expected)
            }
        })
    }
}

Вот так вот. Кажется, ерунда, но когда у тебя таких функций овердохуища, а не одна Sum, эти тесты спасают твою жопу от профессионального выгорания и криков коллег. Запускаешь go test ./... и спишь спокойно. Ну, почти.