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

Тестирование ошибок и паник Go: Автоматическое тестирование

Ошибки и паники — это два разных уровня поломок в программе. Ошибка — это ожидаемая ситуация, которую можно обработать. Пользователь ввёл кривые данные, файл не открылся, база недоступна — всё это нормально, функция вернула ошибку, а вызывающий код решает, что с ней делать.

Паника — это авария. Программа оказалась в состоянии, где продолжать работу уже нельзя. Например, деление на ноль в функции, которая по контракту должна «всегда делить», или сломанное внутреннее состояние. В Go паника выбивается с помощью panic.

В тестах важно проверять и то, и другое: иногда функция должна вернуть ошибку, а иногда она обязана упасть.

Ошибки: встроенный тип error

В Go ошибки — это обычные значения. Есть встроенный интерфейс:

type error interface {
    Error() string
}

То есть всё, что умеет возвращать строку через метод Error(), считается ошибкой. Чаще всего используют errors.New("...") или fmt.Errorf("..."), чтобы создать ошибку.

Функции обычно возвращают результат и ошибку:

package calc

import "errors"

// Divide делит одно число на другое.
// Если знаменатель равен нулю — возвращаем ошибку.
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

Если всё хорошо — ошибка равна nil. Если что-то пошло не так — вторая переменная хранит ошибку с описанием.

Проверка ошибок в тестах

Когда мы пишем тест, важно проверить не только «счастливый путь», но и то, как функция реагирует на неправильный ввод.

package calc_test

import (
    "calc"
    "testing"
)

func TestDivide_Error(t *testing.T) {
    _, err := calc.Divide(10, 0)

    // Проверка №1: ошибка должна быть.
    // Если её нет — дальше проверять нечего.
    if err == nil {
        t.Fatal("ожидали ошибку, но получили nil")
    }

    // Проверка №2: если ошибка есть, проверяем её текст.
    // Сравнение через err.Error().
    want := "division by zero"
    if err.Error() != want {
        t.Errorf("получили %q, а хотели %q", err.Error(), want)
    }
}

Почему t.Fatal и t.Errorf разные?

  • t.Fatalt.Fatalf) — это экстренный стоп. Тест немедленно прерывается. Мы используем его там, где без этого дальше нет смысла. В примере выше — если ошибки вообще нет, то сравнивать её текст бессмысленно.
  • t.Errort.Errorf) — это мягкий сигнал: «что-то не так, но давай посмотрим дальше». Тест помечается как упавший, но выполнение продолжается. Это удобно, если ошибка есть, но сообщение отличается — тогда мы хотя бы увидим, что ещё делал тест.

Нужно запомнить правило: Fatal — для критичных проверок, Error — для уточняющих.

Паники: аварийные ситуации

Иногда функция должна не вернуть ошибку, а «завалиться» с паникой. Это используется редко, но бывают такие места, где программа не имеет смысла без аварийной остановки.

package calc

// MustDivide работает строго: если знаменатель 0 — сразу паника.
func MustDivide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

Проверка паник в тестах

Если такую функцию вызвать напрямую, весь тест упадёт. Чтобы проверить панику, нужно её поймать. Для этого есть связка defer + recover.

func TestMustDivide_Panic(t *testing.T) {
    defer func() {
        // recover ловит панику, если она была.
        if r := recover(); r == nil {
            t.Fatal("ожидали панику, но её не было")
        } else if r != "division by zero" {
            t.Errorf("неожиданное сообщение паники: %v", r)
        }
    }()

    // Этот вызов должен вызвать панику.
    // Если паники не будет, defer не сработает как мы хотим.
    _ = calc.MustDivide(10, 0)
}

Схема работы простая:

  1. Ставим defer с анонимной функцией.
  2. Внутри неё вызываем recover().
  3. Если recover() вернул nil → паники не было → тест провален.
  4. Если вернул строку → проверяем, что она совпадает с ожидаемой.

Когда ошибка, а когда паника?

Ошибки — это часть нормального хода программы. Мы можем их обработать и продолжить работу:

  • Пользователь не ввёл пароль → показали сообщение.
  • Файл не найден → создали новый.
  • Подключение к БД не вышло → повторили через секунду.

Паники — это аварии. Тут уже нельзя «показать подсказку и продолжить». Надо валить выполнение или хотя бы остановить конкретный кусок.

  • Нарушены внутренние инварианты.
  • В функцию передали данные, с которыми она категорически не умеет работать.
  • Ошибка в инициализации, без которой приложение бессмысленно.

В тестах важно фиксировать и такие сценарии: что код не просто «как-то себя ведёт», а строго выдаёт ошибку или строго падает.

Ошибки в Go — это встроенный механизм, с которым мы сталкиваемся постоянно. Паники — более редкий инструмент, но их тоже нужно уметь проверять.

Ошибки проверяются через if err != nil и err.Error(). Паники — через defer и recover. В тестах t.Fatal используют для критичных проверок, когда без этого тест теряет смысл, а t.Errorf — для уточняющих сравнений.

Так мы получаем тесты, которые покрывают весь спектр: и удачные сценарии, и ошибки, и аварийные ситуации.


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

Ниже две функции с разным поведением: одна возвращает ошибку, другая — паникует. Ваша задача — реализовать их и написать тесты.

1) Функция, которая валидирует имя и возвращает ошибку для пустой строки: ```go package validate

import "errors"

var ErrEmptyName = errors.New("empty name")

func ValidateName(name string) error { if name == "" { return ErrEmptyName } return nil } ```

2) Функция, которая извлекает элемент по индексу и паникует при выходе за границы: ```go package safe

func MustAtT any T { if i < 0 || i >= len(xs) { panic("index out of range") } return xs[i] } ```

Что проверить в тестах:

  • ValidateName: ошибка на пустой строке, отсутствие ошибки на непустой.
  • MustAt: корректное значение для валидного индекса; паника при индексе <0 и индексе >= len(xs).

Подсказка: для проверки паник используйте конструкцию с defer и recover().

Показать решение
package validate

import "testing"

func TestValidateName_Empty(t *testing.T) {
    if err := ValidateName(""); err == nil {
        t.Fatalf("expected error, got nil")
    } else if err != ErrEmptyName {
        t.Fatalf("unexpected error: %v", err)
    }
}

func TestValidateName_NonEmpty(t *testing.T) {
    if err := ValidateName("Hexlet"); err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
}
// файл safe/mustat_test.go
            package safe

            import "testing"

            func TestMustAt_Valid(t *testing.T) {
                xs := []int{10, 20, 30}
                got := MustAt(xs, 1)
                if got != 20 {
                    t.Fatalf("got %d, want %d", got, 20)
                }
            }

            func TestMustAt_Panic_NegativeIndex(t *testing.T) {
                xs := []int{1}
                defer func() {
                    if r := recover(); r == nil {
                        t.Fatalf("expected panic, got none")
                    }
                }()
                _ = MustAt(xs, -1)
            }

            func TestMustAt_Panic_OutOfRange(t *testing.T) {
                xs := []int{1, 2}
                defer func() {
                    if r := recover(); r == nil {
                        t.Fatalf("expected panic, got none")
                    }
                }()
                _ = MustAt(xs, 2)
            }

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

  1. errors — стандартный пакет
  2. Defer, panic и recover (статья)
  3. Error handling and Go (статья)
  4. testing — документация

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

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

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

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

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

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

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

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