Небольшие правила — большая разница
Хорошие тесты следуют нескольким привычкам:
1. Понятные имена — TestGreetEmptyName говорит, что сломалось, не открывая файл.
2. Одно поведение на тест — маленькие тесты указывают прямо на проблему.
3. Таблицы для множества случаев — один цикл вместо скопированных тестов.
4. Сообщения с got и want — падение теста должно объяснять себя само.
5. t.Error, чтобы увидеть всё, t.Fatal, когда продолжать бессмысленно.
6. Запускайте тесты часто — после каждого небольшого изменения, а не один раз в конце.
7. Тесты живут рядом с кодом — та же папка, суффикс _test.go.
Сообщения о падении, которые помогают
Упавший тест читают в момент замешательства — сообщение должно нести всю историю: какой вызов, что вернулось, что ожидалось.
Соглашение Go — Func(input) = got; want want. С %q строки берутся в кавычки, поэтому пропущенные пробелы или знаки препинания видны сразу.
Сообщение со всей историей
Greet возвращает неправильное приветствие. Сообщение о падении показывает вызов, got и want — ошибка (не то слово, пропущенные запятая и восклицательный знак) видна уже в одном выводе, без открытия кода.
Вывод: программа на Go и захваченный вывод
package greet
import "testing"
// В Greet есть ошибка: не то слово, пропущены запятая и "!".
func Greet(name string) string {
return "Хай " + name
}
func TestGreet(t *testing.T) {
got := Greet("Ana")
want := "Привет, Ana!"
if got != want {
t.Errorf("Greet(%q) = %q; ожидалось %q", "Ana", got, want)
}
}
Одно поведение на один тест
Тест, который проверяет пять вещей, имеет пять причин упасть — а его имя может описать только одну из них. Разделение по поведению делает каждый тест маленьким, а его имя — честным.
Здесь обычное приветствие и запасной вариант для пустого имени — это два поведения, поэтому им достаются два теста. Когда один падает, его имя уже говорит, какое поведение сломалось.
Два поведения — два теста
TestGreetName проверяет обычный случай, TestGreetEmpty — запасной вариант. Подробный вывод перечисляет их по отдельности — падение указало бы ровно на одно поведение.
Вывод: программа на Go и захваченный вывод
package greet
import "testing"
func Greet(name string) string {
if name == "" {
return "Привет, незнакомец!"
}
return "Привет, " + name + "!"
}
func TestGreetName(t *testing.T) {
got := Greet("Ana")
if got != "Привет, Ana!" {
t.Errorf("Greet(%q) = %q; ожидалось %q", "Ana", got, "Привет, Ana!")
}
}
func TestGreetEmpty(t *testing.T) {
got := Greet("")
if got != "Привет, незнакомец!" {
t.Errorf("Greet(%q) = %q; ожидалось %q", "", got, "Привет, незнакомец!")
}
}
Сколько кода протестировано: покрытие
go test -cover измеряет, какую часть вашего кода тесты действительно выполняют, в процентах от инструкций.
Это число — компас, а не цель: низкое значение показывает непроверенные пути — здесь тест проходит только по ветке mild, поэтому ветки freezing и hot не проверяются никогда. Покрытие в 100% не доказывает, что код правильный, но непокрытые строки гарантированно не проверены.
Измеряем покрытие
У Describe три ветки, а тест задействует одну. Число покрытия это раскрывает — тесты для холодной и жаркой веток подняли бы его.
Вывод: программа на Go и захваченный вывод
package temp
import "testing"
func Describe(celsius int) string {
if celsius < 0 {
return "freezing"
}
if celsius > 30 {
return "hot"
}
return "mild"
}
func TestDescribeMild(t *testing.T) {
got := Describe(20)
if got != "mild" {
t.Errorf("Describe(20) = %q; ожидалось %q", got, "mild")
}
}