Ты наверняка слышал классическую шутку про программиста, у которого стопроцентное покрытие тестами, но при первом же запуске в продакшене сервис падает с паникой. Мы уже обсуждали эту иллюзию в материале о том, почему зелёный линтер не гарантирует надежность тестов. В модульном тестировании ситуация аналогичная: зелёная плашка в терминале сама по себе не доказывает, что твой тест умеет ловить баги.
Известный инженер и автор книги «The Power of Go: Tests» Джон Арундел (John Arundel) сформулировал этот парадокс предельно наглядно: тест, который ни разу не падал на твоих глазах, не заслуживает доверия.
Давай разберём классический цикл TDD (Test-Driven Development) на примере, казалось бы, тривиальной задачи — функции IsSorted, проверяющей, отсортирован ли срез целых чисел по возрастанию.
Программирование задом наперёд: от красного к зелёному
Как поступает разработчик без TDD? Он сразу пишет цикл for, сравнивает элементы и затем набрасывает проверку на []int{1, 2, 3}. Тест проходит. Все довольны.
А что, если в коде закралась опечатка или функция вообще состоит из одной строчки return true? Тест всё равно останется зелёным! Мы получили ложное чувство безопасности.
В TDD мы переворачиваем процесс с головы на ноги:
- Красная фаза (Red). Мы объявляем желаемый вызов
sorted.IsSorted(input)и пишем минимальную заглушку для компиляции, которая возвращает заведомо неверный ответ:return false. Запускаемgo testи лично убеждаемся: тест упал с понятным сообщением об ошибке. Тест живой! - Симметричная проверка. Добавляем кейс на неотсортированные данные
[]int{1, 3, 2} -> false. Теперь ни одна константа (trueилиfalse) не сможет обмануть наш тестовый раннер. - Зелёная фаза (Shameless Green). Пишем простейший рабочий код, даже если он кажется наивным.
Эволюция пограничных условий алгоритма
Именно при переходе от наивного кода к продакшену вскрываются классические грабли алгоритма:
- Отрицательные числа: если инициализировать переменную предыдущего значения нулём (
prev := 0), срез[]int{-1, 2, 3}ошибочно посчитается неотсортированным (-1 < 0). Инициализировать нужно строго первым элементом среза! - Пустые срезы: обращение
input[0]при пустом срезе[]int{}немедленно приведёт к фатальной панике времени выполненияpanic: runtime error: index out of range. Требуется граничная защитаlen(input) < 2. - Дубликаты: элементы
[]int{1, 2, 2}должны считаться отсортированными, условие сбоя — строгоv < prev.
Табличные тесты и параллелизм в Go 1.22+
Идеальный паттерн для таких проверок в Go — табличные тесты (table-driven tests) в сочетании с подтестами t.Run и параллельным запуском t.Parallel():
package sorted_test
import (
"testing"
"github.com/example/sorted"
)
func TestIsSorted_TableDriven(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input []int
want bool
}{
{"отсортированный срез", []int{1, 2, 3}, true},
{"повторяющиеся элементы", []int{1, 2, 2}, true},
{"отрицательные числа", []int{-1, 2, 3}, true},
{"пустой срез", []int{}, true},
{"один элемент", []int{42}, true},
{"нарушение в конце", []int{1, 3, 2}, false},
{"обратный порядок", []int{3, 2, 1}, false},
}
for _, tc := range tests {
// В Go 1.22+ затенение tc := tc больше не требуется:
// переменные цикла теперь изолированы на каждой итерации
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := sorted.IsSorted(tc.input)
if got != tc.want {
t.Errorf("IsSorted(%v) = %v, ожидалось %v", tc.input, got, tc.want)
}
})
}
}
Фаза рефакторинга: переход на slices.IsSorted
Когда все пограничные сценарии надёжно зафиксированы тестами, наступает любимый этап любого инженера — рефакторинг. Зачем поддерживать собственный рукописный цикл, если в стандартной библиотеке Go 1.21+ появился generic-пакет slices?
Мы просто выбрасываем свой код и заменяем его вызовом библиотечной функции:
package sorted
import "slices"
// IsSorted проверяет упорядоченность элементов любого сравнимого типа
func IsSorted(input []int) bool {
return slices.IsSorted(input)
}
Запускаем команду go test -v -race ./... — все семь параллельных подтестов горят зелёным. Мы провели полный рефакторинг кодовой базы за пару секунд, будучи на 100% уверенными, что ни одно граничное условие не сломано. В этом и заключается истинная суперсила TDD.
