В программировании есть золотое правило: доверяй, но проверяй. Даже если программа работает «на глаз», это не значит, что она работает правильно. Ошибки могут быть незаметными, проявляться только в определённых условиях или после изменений в коде. Чтобы защитить себя и команду, разработчики пишут unit-тесты — небольшие автоматические проверки отдельных функций или модулей.
Как устроены тесты в Go
В Go тесты — это такие же .go
файлы, только с особым именем. Если файл заканчивается на _test.go
, компилятор понимает: внутри находятся тесты. При запуске команды go test
язык автоматически находит все такие файлы, компилирует их вместе с кодом и выполняет.
Тестовая функция тоже имеет свои правила. Она должна называться TestИмя
, начинаться с большой буквы и принимать аргумент t *testing.T
. Через этот объект мы можем сообщать об ошибках, если функция работает не так, как ожидалось. Шаблон выглядит так:
func TestИмяФункции(t *testing.T) {
// проверки
}
Go сам найдёт такие функции и запустит их при go test
.
Первый тест
Допустим, у нас есть простая функция, которая складывает два числа:
// файл calc.go
package calc
func Sum(a, b int) int {
return a + b
}
Теперь создаём рядом файл calc_test.go
. Его имя показывает, что внутри тесты:
// файл calc_test.go
package calc
import "testing"
func TestSum(t *testing.T) {
got := Sum(2, 3) // вызываем нашу функцию
want := 5 // ожидаем правильный результат
if got != want { // сравниваем
// t.Errorf сообщает об ошибке, если ожидания не совпали
t.Errorf("Sum(2,3) = %d; want %d", got, want)
}
}
В языке Go каждая тестовая функция принимает аргумент t *testing.T
. Этот объект используется для взаимодействия теста с системой тестирования. У него есть разные методы для различных ситуаций.
t.Log
печатает сообщение в логи.t.Error
фиксирует ошибку. Тест продолжает выполняться, но будет считаться проваленным.t.Errorf
работает так же, какt.Error
, но поддерживает форматирование строк по аналогии сfmt.Printf
.t.Fatal
фиксирует ошибку и сразу завершает выполнение теста.t.Fatalf
делает то же самое, чтоt.Fatal
, но с форматированием.
Таким образом, t.Errorf
— это способ сообщить об ошибке в тесте, при этом подставив переменные прямо в текст сообщения.
Пример
package calc
import "testing"
func Sum(a, b int) int {
return a + b
}
func TestSum(t *testing.T) {
got := Sum(2, 3)
want := 6 // специально ошибка
// если результат не совпадает, выводим сообщение об ошибке
if got != want {
t.Errorf("Sum(2, 3) = %d; want %d", got, want)
}
}
При запуске:
--- FAIL: TestSum (0.00s)
calc_test.go:11: Sum(2, 3) = 5; want 6
FAIL
В отчёте видно: тест не прошёл, полученное значение равно 5
, а ожидалось 6
.
Если бы вместо этого использовался вызов t.Error("что-то пошло не так")
, то в выводе появилась бы только эта фраза без конкретных чисел. Поэтому t.Errorf
предпочтительнее: он делает сообщения точными и информативными.
Запускаем тесты
Всё, что нужно, — написать код и тест. Дальше запускаем команду:
go test
Если всё верно, вывод будет таким:
ok имя_пакета 0.002s
Если намеренно поменять ожидание, например want := 6
, то тест упадёт и покажет ошибку:
--- FAIL: TestSum (0.00s)
calc_test.go:10: Sum(2,3) = 5; want 6
FAIL
Так мы видим, что функция не соответствует ожиданиям.
Структура тестов в проекте
Когда тестов становится много, важно держать порядок:
Файл _test.go
всегда лежит рядом с кодом. Если у нас есть calc.go
, рядом будет calc_test.go
. Так легче находить и сопровождать тесты.
Название теста совпадает с функцией, которую проверяем. Тест на функцию Sum
— TestSum
. Это сразу ясно при чтении.
Один тест проверяет один сценарий. Если нужно покрыть разные случаи, можно написать несколько функций или использовать табличные тесты (о них позже).
Читаемость важнее всего. Тест — это тоже код. Его должен уметь понять любой разработчик, даже тот, кто пишет в проекте первый день.
Весь процесс сводится к простым шагам: написать функцию, создать рядом файл _test.go
, описать Test...
и проверить результат через t.Errorf
. Даже самые простые проверки позволяют заметить ошибки сразу и не тратить время на ручные проверки. С этого начинается тестирование кода.
Самостоятельная работа
Чтобы сразу закрепить материал, подготовим учебный репозиторий, где в ходе курса будем добавлять функции и покрывать их тестами. Автоматический запуск тестов настроим через GitHub Actions.
- Создайте публичный репозиторий на GitHub, например
go-testing
. Инициализируйте проект локально:
mkdir go-testing && cd go-testing go mod init github.com/<ваш_ник>/go-testing
Добавьте простой код и тест, чтобы проверить пайплайн:
// файл hello/hello.go package hello func Hello(name string) string { if name == "" { return "Hello, world!" } return "Hello, " + name + "!" }
// файл hello/hello_test.go package hello import "testing" func TestHello(t *testing.T) { if got, want := Hello("Hexlet"), "Hello, Hexlet!"; got != want { t.Errorf("got %q, want %q", got, want) } }
Настройте GitHub Actions для автоматического запуска тестов при каждом пуше и pull request. Создайте файл
.github/workflows/ci.yml
со следующим содержимым:name: CI on: push: pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.25.x' cache: true - name: Run tests run: go test ./...
Закоммитьте и запушьте изменения. Убедитесь, что пайплайн в GitHub Actions успешно проходит.
В следующих уроках вы будете постепенно развивать этот проект, добавляя новые функции и тесты. CI гарантирует, что каждый шаг сохраняет корректность кода.
Первый тест
Напишите функцию IsEven(n int) bool
, проверяет, является ли число четным.
Что сделать:
- Создайте пакет, например
even
с функцией. - Напишите unit‑тесты в файле в отдельном файле.
- Покройте случаи пограничные случаи.
Показать решение
package even
import "testing"
func TestIsEven(t *testing.T) {
if !isEven(2) {
t.Error("Ожидалось true для 2, но получено false")
}
if isEven(3) {
t.Error("Ожидалось false для 3, но получено true")
}
if !isEven(0) {
t.Error("Ожидалось true для 0, но получено false")
}
if !isEven(-4) {
t.Error("Ожидалось true для -4, но получено false")
}
if isEven(-7) {
t.Error("Ожидалось false для -7, но получено true")
}
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.