Зарегистрируйтесь, чтобы продолжить обучение

Первый unit-тест и структура тестов Go: Автоматическое тестирование

В программировании есть золотое правило: доверяй, но проверяй. Даже если программа работает «на глаз», это не значит, что она работает правильно. Ошибки могут быть незаметными, проявляться только в определённых условиях или после изменений в коде. Чтобы защитить себя и команду, разработчики пишут 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. Так легче находить и сопровождать тесты.

Название теста совпадает с функцией, которую проверяем. Тест на функцию SumTestSum. Это сразу ясно при чтении.

Один тест проверяет один сценарий. Если нужно покрыть разные случаи, можно написать несколько функций или использовать табличные тесты (о них позже).

Читаемость важнее всего. Тест — это тоже код. Его должен уметь понять любой разработчик, даже тот, кто пишет в проекте первый день.

Весь процесс сводится к простым шагам: написать функцию, создать рядом файл _test.go, описать Test... и проверить результат через t.Errorf. Даже самые простые проверки позволяют заметить ошибки сразу и не тратить время на ручные проверки. С этого начинается тестирование кода.


Самостоятельная работа

Чтобы сразу закрепить материал, подготовим учебный репозиторий, где в ходе курса будем добавлять функции и покрывать их тестами. Автоматический запуск тестов настроим через GitHub Actions.

  1. Создайте публичный репозиторий на GitHub, например go-testing.
  2. Инициализируйте проект локально:

    mkdir go-testing && cd go-testing
    go mod init github.com/<ваш_ник>/go-testing
    
  3. Добавьте простой код и тест, чтобы проверить пайплайн:

    // файл 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)
        }
    }
    
  4. Настройте 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 ./...
    
  5. Закоммитьте и запушьте изменения. Убедитесь, что пайплайн в 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")
    }
}

Дополнительные материалы

  1. Документация пакета testing
  2. Учебник — добавляем первый тест
  3. Effective Go — раздел Testing

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff