Go: Автоматическое тестирование

Теория: Библиотека testify: ассерты и моки

В стандартном пакете 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 делает тесты более выразительными и поддерживаемыми. Вместо длинных условий и повторяющегося кода остаются простые и понятные утверждения, которые показывают суть проверки.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845