- Названия тестов
- Структура тестов
- Использование табличных тестов
- Хелперы для повторяющихся проверок
- Независимость тестов
- Параллельные тесты
- Использование сторонних библиотек
- Итоги
Когда тестов становится несколько десятков, они ещё кажутся управляемыми. Но как только их число идёт на сотни, всё начинает зависеть от того, насколько аккуратно они написаны и организованы. Читаемость и структура становятся критичнее самой логики. Плохо организованные тесты превращаются в хаос: трудно понять, что именно проверяется, почему упал тест и как его починить.
Хорошо написанный тест читается как маленькая история. В нём ясно, какие входные данные использованы, какой результат ожидается и почему это важно. Такой тест не просто проверяет программу, но и документирует её поведение.
Названия тестов
Название теста — это первое, что видит разработчик при падении. Оно должно сразу объяснять, что проверяется. Вместо Test1
или TestFunc
лучше писать конкретно:
func TestDivide_ByZero_ReturnsError(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Fatal("ожидали ошибку при делении на ноль")
}
}
Название сразу говорит: проверяется деление на ноль, ожидается ошибка. При падении будет очевидно, о чём речь.
Структура тестов
Полезно придерживаться одинакового ритма: подготовка → действие → проверка.
Пример:
func TestMax(t *testing.T) {
// подготовка входных данных
a, b := 2, 3
// действие
got := Max(a, b)
// проверка
want := 3
if got != want {
t.Errorf("ожидали %d, получили %d", want, got)
}
}
Такой порядок облегчает чтение: даже не глядя в детали, понятно, что вначале создаются данные, потом вызывается функция, потом идёт проверка.
Использование табличных тестов
Если вариантов входных данных много, отдельные тесты быстро превращаются в дубли. Табличные тесты позволяют перечислить сценарии в срезе и пройтись по ним в цикле.
func TestIsEven(t *testing.T) {
cases := []struct {
n int
want bool
}{
{2, true},
{3, false},
{10, true},
}
for _, c := range cases {
got := IsEven(c.n)
if got != c.want {
t.Errorf("IsEven(%d) = %v, хотели %v", c.n, got, c.want)
}
}
}
Один тест проверяет сразу несколько сценариев, и читать его легче.
Хелперы для повторяющихся проверок
Если в тестах часто повторяется одна и та же проверка, лучше вынести её в функцию-хелпер.
func assertEqual(t testing.TB, got, want int) {
t.Helper()
if got != want {
t.Errorf("получили %d, хотели %d", got, want)
}
}
Теперь любой тест можно упростить:
func TestMax_Helper(t *testing.T) {
assertEqual(t, Max(2, 3), 3)
assertEqual(t, Max(10, 5), 10)
}
Независимость тестов
Тесты должны быть изолированы друг от друга. Один тест не должен полагаться на результаты другого. Если тесты используют файлы, лучше создавать временные с помощью t.TempDir()
. Если обращаются к базе или сетевым ресурсам — поднимать для каждого свой изолированный стенд или подменять зависимости моками.
Параллельные тесты
t.Parallel()
ускоряет выполнение большого набора тестов. Но чтобы избежать гонок данных, нужно помнить: каждый тест должен работать только со своими данными или синхронизировать доступ к общим ресурсам через мьютексы.
Использование сторонних библиотек
Библиотеки вроде testify
делают тесты компактнее и читаемее. Ассерты помогают выразить проверки в одной строке, а моки позволяют быстро подменить зависимости. Но важно помнить: библиотека — лишь инструмент. Главная цель остаётся прежней — читаемый тест, который ясно показывает намерение.
Итоги
Лучшие тесты — это те, которые легко читать. Названия функций должны быть понятными, структура предсказуемой, данные — простыми, проверки — очевидными. Табличные тесты, хелперы и моки помогают избавиться от дублирования и лишнего шума. Независимость и параллельность делают тесты быстрыми и надёжными.
Хорошо организованный набор тестов становится не только защитой от ошибок, но и живой документацией: он показывает, как должна вести себя программа в разных ситуациях.
Самостоятельная работа
Потренируемся писать «чистые» тесты на двух небольших функциях.
package textutil
import (
"bufio"
"io"
"strings"
)
// NormalizeSpaces схлопывает последовательности пробельных символов в один пробел
// и обрезает крайние пробелы.
func NormalizeSpaces(s string) string {
tokens := strings.Fields(s)
return strings.Join(tokens, " ")
}
// CountLines считает количество строк в ридере (по разделителю \n).
func CountLines(r io.Reader) (int, error) {
var n int
scanner := bufio.NewScanner(r)
for scanner.Scan() {
n++
}
return n, scanner.Err()
}
Напишите тесты к функциям.
Что покрыть в тестах
- NormalizeSpaces:
- пустая строка;
- только пробелы/табы;
- смешанные пробелы и текст, уже нормализованный текст;
- юникод и пунктуация сохраняются как есть.
- CountLines:
- пустой ввод (0 строк);
- одна строка без завершающего перевода;
- несколько строк, включая последнюю с \n и без него.
Подсказка
- Вынесите сравнение строк в хелпер с
t.Helper()
. - Используйте
t.Run
+ табличные тесты, а для независимых сценариев —t.Parallel()
. - Для файлов вместо диска используйте
strings.NewReader
илиbytes.NewBuffer
.
Показать решение
package textutil
import (
"strings"
"testing"
)
func assertEq(t testing.TB, got, want string) {
t.Helper()
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
}
func TestNormalizeSpaces(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in string
want string
}{
{name: "empty", in: "", want: ""},
{name: "spaces_tabs", in: " a\tb ", want: "a b"},
{name: "already_clean", in: "a b c", want: "a b c"},
{name: "unicode_punct", in: " Привет, мир! ", want: "Привет, мир!"},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := NormalizeSpaces(tc.in)
assertEq(t, got, tc.want)
})
}
}
func TestCountLines(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in string
want int
}{
{name: "empty", in: "", want: 0},
{name: "single_no_nl", in: "line", want: 1},
{name: "two_with_nl", in: "a\n\n", want: 2},
{name: "three_mixed", in: "a\nb\nc", want: 3},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := CountLines(strings.NewReader(tc.in))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tc.want {
t.Fatalf("%s: got %d, want %d", tc.name, got, tc.want)
}
})
}
}
Дополнительные материалы
- Table-driven tests (Go Wiki)
- Uber Go Style Guide — Testing
- Code Review Comments — общие рекомендации
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.