Что такое Testify и зачем он нужен?
Testify — это одна из самых популярных библиотек для тестирования на языке Go. Она расширяет возможности стандартного пакета testing, предоставляя разработчикам мощные утилиты для написания чистых, читаемых и поддерживаемых тестов. Testify решает несколько ключевых проблем, с которыми сталкиваются Go-разработчики:
- Упрощение утверждений (assertions): Вместо громоздких конструкций с
ifиt.Errorf, Testify предлагает лаконичные методы вродеassert.Equal. - Читаемость ошибок: При провале теста Testify выводит подробные сообщения, показывающие ожидаемые и фактические значения.
- Создание моков (mocks): Встроенная поддержка моков позволяет легко изолировать тестируемый код от внешних зависимостей (базы данных, HTTP-клиенты, файловые системы).
- Группировка тестов (suite): Позволяет объединять тесты в логические группы с общими настройками (Setup/Teardown).
Testify идеально подходит для проектов любого размера — от небольших утилит до крупных микросервисных архитектур. Она является де-факто стандартом для тестирования в Go-сообществе.
Установка
Testify устанавливается как обычная Go-зависимость. Убедитесь, что у вас установлен Go (версия 1.16 или выше) и инициализирован модуль (go mod init).
go get github.com/stretchr/testify
После выполнения команды Testify будет добавлен в ваш go.mod файл. Теперь вы можете импортировать его в тестовые файлы.
Быстрый старт
Давайте напишем простой тест для функции, которая складывает два числа. Создадим файл math.go:
package main
func Add(a, b int) int {
return a + b
}
Теперь создадим тестовый файл math_test.go:
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
assert.Equal(t, expected, result, "2 + 3 должно быть равно 5")
}
Запустите тест:
go test -v
Вы увидите зеленый результат. Если изменить ожидаемое значение на 6, тест упадет с подробным сообщением об ошибке.
Основные методы и функции
1. assert.Equal
Описание: Проверяет, что два значения равны. Использует reflect.DeepEqual для сравнения сложных типов (слайсы, структуры, карты).
Параметры:
t— объект *testing.Texpected— ожидаемое значениеactual— фактическое значениеmsg(опционально) — сообщение об ошибке
Пример:
func TestEqual(t *testing.T) {
assert.Equal(t, 10, 5+5, "5+5 должно быть 10")
assert.Equal(t, []int{1, 2, 3}, []int{1, 2, 3})
}
2. assert.NotEqual
Описание: Противоположность Equal. Проверяет, что два значения НЕ равны.
Параметры: Аналогичны Equal.
Пример:
func TestNotEqual(t *testing.T) {
assert.NotEqual(t, 10, 5+4, "5+4 не равно 10")
}
3. assert.True / assert.False
Описание: Проверяет, что выражение истинно (True) или ложно (False).
Параметры:
t— объект *testing.Tcondition— булево выражениеmsg(опционально) — сообщение об ошибке
Пример:
func TestTrueFalse(t *testing.T) {
assert.True(t, 5 > 3, "5 должно быть больше 3")
assert.False(t, 5 < 3, "5 не должно быть меньше 3")
}
4. assert.Nil / assert.NotNil
Описание: Проверяет, что значение равно nil (Nil) или не равно nil (NotNil). Полезно для проверки ошибок.
Параметры: Аналогичны True.
Пример:
func TestNil(t *testing.T) {
var err error = nil
assert.Nil(t, err, "Ошибки быть не должно")
err = errors.New("some error")
assert.NotNil(t, err, "Ошибка должна быть")
}
5. assert.Contains / assert.NotContains
Описание: Проверяет, содержит ли строка, слайс или карта указанный элемент (Contains) или не содержит (NotContains).
Параметры:
t— объект *testing.Tcontainer— контейнер (строка, слайс, карта)element— искомый элементmsg(опционально) — сообщение об ошибке
Пример:
func TestContains(t *testing.T) {
assert.Contains(t, "Hello, World!", "World")
assert.NotContains(t, "Hello, World!", "Go")
assert.Contains(t, []int{1, 2, 3}, 2)
}
6. assert.Len
Описание: Проверяет, что длина строки, слайса, карты или массива равна ожидаемой.
Параметры:
t— объект *testing.Tobject— объект, длину которого проверяемlength— ожидаемая длинаmsg(опционально) — сообщение об ошибке
Пример:
func TestLen(t *testing.T) {
assert.Len(t, "Go", 2)
assert.Len(t, []int{1, 2, 3}, 3)
assert.Len(t, map[string]int{"a": 1}, 1)
}
7. assert.Error / assert.NoError
Описание: Проверяет, что ошибка не равна nil (Error) или равна nil (NoError). Критически важно для тестирования функций, возвращающих ошибки.
Параметры:
t— объект *testing.Terr— ошибкаmsg(опционально) — сообщение об ошибке
Пример:
func TestError(t *testing.T) {
_, err := strconv.Atoi("abc")
assert.Error(t, err, "Должна возникнуть ошибка")
_, err = strconv.Atoi("123")
assert.NoError(t, err, "Ошибки быть не должно")
}
8. assert.Panics / assert.NotPanics
Описание: Проверяет, что функция вызывает панику (Panics) или не вызывает (NotPanics). Полезно для тестирования защитного программирования.
Параметры:
t— объект *testing.Tfn— функция, которая может вызвать панику (обычно анонимная)msg(опционально) — сообщение об ошибке
Пример:
func TestPanics(t *testing.T) {
assert.Panics(t, func() {
panic("something went wrong")
}, "Функция должна вызвать панику")
assert.NotPanics(t, func() {
_ = 1 + 1
}, "Функция не должна вызвать панику")
}
9. assert.InDelta
Описание: Проверяет, что два числа (float64) находятся в пределах заданной дельты друг от друга. Полезно для тестирования вычислений с плавающей точкой.
Параметры:
t— объект *testing.Texpected— ожидаемое значениеactual— фактическое значениеdelta— максимально допустимая разницаmsg(опционально) — сообщение об ошибке
Пример:
func TestInDelta(t *testing.T) {
result := 10.0 / 3.0
assert.InDelta(t, 3.333, result, 0.001, "Результат должен быть близок к 3.333")
}
10. assert.IsType
Описание: Проверяет, что фактическое значение имеет ожидаемый тип.
Параметры:
t— объект *testing.TexpectedType— значение ожидаемого типа (обычно nil указатель на тип)actual— фактическое значениеmsg(опционально) — сообщение об ошибке
Пример:
func TestIsType(t *testing.T) {
var val interface{} = "hello"
assert.IsType(t, "", val, "val должен быть строкой")
val = 42
assert.IsType(t, 0, val, "val должен быть int")
}
Реальный практический пример: тестирование HTTP-сервиса
Рассмотрим реальный сценарий: у нас есть HTTP-сервис, который сохраняет пользователей. Мы будем использовать моки для изоляции от базы данных.
Структура проекта:
user.go— модель пользователяstorage.go— интерфейс хранилищаhandler.go— HTTP-обработчикhandler_test.go— тесты
user.go:
package main
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
storage.go:
package main
type Storage interface {
SaveUser(user User) error
GetUser(id int) (User, error)
}
handler.go:
package main
import (
"encoding/json"
"net/http"
"strconv"
)
type UserHandler struct {
storage Storage
}
func NewUserHandler(storage Storage) *UserHandler {
return &UserHandler{storage: storage}
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if user.Name == "" {
http.Error(w, "name is required", http.StatusBadRequest)
return