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

Библиотека testify: ассерты и моки Go: Автоматическое тестирование

В стандартном пакете testing проверки строятся вокруг выражений вида if got != want { t.Errorf(...) }. Это надёжный способ, но при большом количестве тестов он делает код громоздким и плохо читаемым. Чтобы сделать тесты короче и понятнее, в Go часто используют библиотеку testify. Она предоставляет два основных инструмента:

  1. Набор функций для удобных проверок — ассерты (assert) и жёсткие проверки (require). \
  2. Пакет для создания и настройки моков (mock).

Ассерты

Ассерты — это готовые функции, которые проверяют условие и при его нарушении помечают тест как упавший. Они заменяют ручные проверки и делают код лаконичным.

Простейший пример:

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestSum(t *testing.T) {
    got := 2 + 2
    assert.Equal(t, 4, got) // ожидаем, что результат равен 4
}

Раньше здесь пришлось бы писать условие и выводить сообщение через t.Errorf. Теперь тест читается как простое утверждение.

Проверки со строками

Ассерты особенно удобны при работе со строками.

func TestHello(t *testing.T) {
    msg := "hello" + " world"

    assert.Equal(t, "hello world", msg) // строки равны
    assert.NotEqual(t, "goodbye", msg)  // строки не равны
    assert.Contains(t, msg, "world")    // строка содержит подстроку
    assert.NotContains(t, msg, "cat")   // подстрока отсутствует
}

Код остаётся компактным и легко читается.

Проверки ошибок

Вместо ручных сравнений удобно использовать ассерты для ошибок.

import "fmt"

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

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

    assert.Error(t, err)                          // ошибка есть
    assert.EqualError(t, err, "division by zero") // текст ошибки совпадает
}

Результат тот же, но код проще и яснее.

assert и require

В testify есть два вида проверок:

  • assert сообщает об ошибке, но позволяет тесту продолжить выполнение.
  • require останавливает тест сразу, как t.Fatal.
import "github.com/stretchr/testify/require"

func TestDivideRequire(t *testing.T) {
    _, err := Divide(10, 0)
    require.Error(t, err) // если ошибки нет — тест не продолжится
}

Часто используют комбинацию: сначала критические условия проверяют через require, а уточняющие детали — через assert.

Табличные тесты

Ассерты хорошо подходят для табличных тестов, где проверяется много входных данных.

func Max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func TestMax(t *testing.T) {
    cases := []struct {
        a, b int
        want int
    }{
        {2, 3, 3},
        {10, 5, 10},
        {7, 7, 7},
    }

    for _, c := range cases {
        got := Max(c.a, c.b)
        assert.Equal(t, c.want, got, "Max(%d, %d)", c.a, c.b)
    }
}

Каждая проверка выражается одной строкой, а сообщения об ошибках показывают, для какого случая тест провалился.

Проверки структур и коллекций

Testify умеет сравнивать структуры, срезы и карты без дополнительного кода.

type User struct {
    ID   int
    Name string
}

func TestStructsAndSlices(t *testing.T) {
    // сравнение структур
    assert.Equal(t, User{1, "Alice"}, User{1, "Alice"})

    // сравнение срезов
    assert.Equal(t, []int{1, 2, 3}, []int{1, 2, 3})

    // если порядок не важен
    assert.ElementsMatch(t, []int{3, 2, 1}, []int{1, 2, 3})
}

Есть отдельные функции для проверки JSON, подмножеств и времени (assert.JSONEq, assert.Subset, assert.WithinDuration).

Моки

Вторая часть testify — это пакет mock, позволяющий создавать поддельные зависимости. Вместо того чтобы писать собственные реализации интерфейсов, можно объявить мок и настраивать его прямо в тесте.

Пример: сервис зависит от хранилища пользователей.

type UserStorage interface {
    GetUser(id int) (string, error)
}

type Service struct {
    storage UserStorage
}

func NewService(storage UserStorage) *Service {
    return &Service{storage: storage}
}

func (s *Service) GetName(id int) (string, error) {
    return s.storage.GetUser(id)
}

Мок на testify:

import "github.com/stretchr/testify/mock"

type MockUserStorage struct {
    mock.Mock
}

func (m *MockUserStorage) GetUser(id int) (string, error) {
    args := m.Called(id)
    return args.String(0), args.Error(1)
}

Тест с использованием мока:

func TestServiceWithMock(t *testing.T) {
    m := new(MockUserStorage)

    // настраиваем поведение: при GetUser(1) вернуть "Alice", nil
    m.On("GetUser", 1).Return("Alice", nil)

    service := NewService(m)
    name, err := service.GetName(1)

    require.NoError(t, err)
    assert.Equal(t, "Alice", name)

    // проверяем, что метод действительно вызывался
    m.AssertCalled(t, "GetUser", 1)
    m.AssertNumberOfCalls(t, "GetUser", 1)
}

Моки можно настраивать для разных входов и разных сценариев:

m.On("GetUser", 1).Return("Alice", nil)
m.On("GetUser", 2).Return("Bob", nil)
m.On("GetUser", 99).Return("", fmt.Errorf("not found"))

Также есть возможность проверять все ожидаемые вызовы через m.AssertExpectations(t). Библиотека testify решает две основные задачи:

  • Ассерты (assert и require) делают проверки короче и понятнее.
  • Моки (mock) позволяют удобно подменять зависимости и контролировать их вызовы.

Использование testify делает тесты более выразительными и поддерживаемыми. Вместо длинных условий и повторяющегося кода остаются простые и понятные утверждения, которые показывают суть проверки.

Практика: https://chatgpt.com/canvas/shared/68d9b6ba62b48191a62aa2c289cf7e46


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

С помощью testify проверим работу функции, которая формирует URL‑дружелюбный идентификатор из произвольной строки.

Для этого создана функция func Slug(s string) string, которая нормализует строку и приводит ее к виду kebab-caseю

  • Подключите библиотеку testify к проекту (импорт в тесте: github.com/stretchr/testify/assert)
  • Напишите табличные тесты на testify/assert.
package sluggy

import (
    "strings"
    "unicode"
)

// Slug нормализует строку для использования в URL.
func Slug(s string) string {
    s = strings.ToLower(s)
    var b strings.Builder
    prevHyphen := false
    for _, r := range s {
        if unicode.IsLetter(r) || unicode.IsDigit(r) {
            b.WriteRune(r)
            prevHyphen = false
            continue
        }
        if !prevHyphen {
            b.WriteByte('-')
            prevHyphen = true
        }
    }
    out := b.String()
    out = strings.Trim(out, "-")
    // Схлопывание уже обеспечено логикой prevHyphen
    return out
}

Что покрыть в тестах

  • Пробелы и пунктуация → дефисы.
  • Повторяющиеся разделители → один дефис.
  • Unicode‑буквы сохраняются и нижний регистр применяется.
  • Пустая строка.

Подсказка

  • Используйте assert.Equal(t, want, got) и под‑тесты через t.Run.
  • Импорт для теста: github.com/stretchr/testify/assert.
Показать решение
package sluggy

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestSlug(t *testing.T) {
    tests := []struct {
        name string
        in   string
        want string
    }{
        {"basic", "Hello World", "hello-world"},
        {"punct", "Go, Dev!", "go-dev"},
        {"dupes", "a---b__c", "a-b-c"},
        {"unicode", "Привет Мир", "привет-мир"},
        {"empty", "", ""},
    }
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            got := Slug(tc.in)
            assert.Equal(t, tc.want, got)
        })
    }
}

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

  1. Testify — репозиторий
  2. assert — документация
  3. require — документация

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

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

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

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

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

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

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

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