Ответ
Unit-тестирование — неотъемлемая часть разработки на Go, и язык предоставляет мощные встроенные инструменты для этого.
Основные инструменты и пакеты
- Пакет
testing: Стандартная библиотека Go. Все тесты пишутся в файлах с суффиксом_test.goи запускаются командойgo test. testify/assertиtestify/require: Самая популярная сторонняя библиотека, которая предоставляет удобные функции для проверок (ассертов).assertпродолжает выполнение теста после ошибки, аrequireнемедленно его прерывает.testify/mockилиgomock: Библиотеки для создания моков (mock objects) — заглушек, которые имитируют поведение внешних зависимостей (например, базы данных или API), позволяя изолировать тестируемый код.
Ключевые подходы и паттерны
-
Table-Driven Tests (Табличные тесты): Это идиоматический подход в Go. Вместо написания отдельной функции для каждого тестового случая, создается срез структур, где каждая структура описывает один тест-кейс (входные данные, ожидаемый результат, название).
-
Мокирование зависимостей: Для тестирования логики, зависящей от внешних систем, используются интерфейсы. В тестах реальная зависимость подменяется моком, который реализует тот же интерфейс и возвращает предопределенные данные.
-
Тестирование на гонки данных (Race Condition): Go имеет встроенный детектор гонок. Запуск тестов с флагом
go test -raceпомогает выявить проблемы конкурентного доступа к памяти, что критически важно для многопоточных приложений. -
Покрытие кода (Code Coverage): Команда
go test -coverпоказывает, какой процент кода покрыт тестами. Можно сгенерировать детальный HTML-отчет:go test -coverprofile=coverage.out && go tool cover -html=coverage.out.
Пример: Табличный тест
Предположим, у нас есть простая функция Sum:
// file: calculator.go
package main
func Sum(a, b int) int {
return a + b
}
Тест для нее будет выглядеть так:
// file: calculator_test.go
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSum(t *testing.T) {
// Описание тестовых случаев
testCases := []struct {
name string // Название теста
a int // Входной параметр 1
b int // Входной параметр 2
want int // Ожидаемый результат
}{
{"Два положительных числа", 2, 3, 5},
{"Положительное и отрицательное", 5, -3, 2},
{"Два отрицательных числа", -1, -1, -2},
{"С нулем", 10, 0, 10},
}
// Итерация по тестовым случаям
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := Sum(tc.a, tc.b)
assert.Equal(t, tc.want, got, "результат должен совпадать с ожидаемым")
})
}
} Ответ 18+ 🔞
Ага, смотри, вот тебе про тесты в Go, но без этой занудной академической хуйни. Сядь, слушай.
Так, значит, ты пишешь код на Go. И ты, конечно, не мудак, который потом руками всё проверяет. Ты пишешь тесты. В Go с этим всё просто, но есть свои приколы, которые надо знать, чтобы не обосраться.
Чем тут можно пользоваться, как инструментами
testing— родной пакет. Это как родная хата, всё есть, но без особых удобств. Пишешь файлы с названиемчто-то_test.go, кричишьgo test, и он всё прогоняет. Без этого нихуя.testify/assertиtestify/require— твои лучшие друзья. Это уже не родная хата, а евроремонт. Вместо того чтобы писатьif got != want { t.Errorf(...) }, ты просто орёшьassert.Equal(t, ожидаемое, полученное). Разница в том, чтоassertпосле ошибки продолжает тест, аrequire— сразу в сраку, прерывает всё. Выбирай по характеру.- Моки (
testify/mockилиgomock). Это когда твой код общается с какой-то внешней хуйнёй — базой данных, другим сервисом. Чтобы не зависеть от того, жива ли эта хуйня, ты создаёшь её муляж — мок. И говоришь этому муляжу: «Слушай, когда к тебе обратятся, сделай вид, что ты база данных, и верни вот это». Идеально для изоляции.
Как не выстрелить себе в ногу: главные подходы
- Табличные тесты (Table-Driven Tests). Это святое, блядь. Не пиши ты десять функций
TestSum_WithPositive,TestSum_WithNegative. Ты чё, совсем? Создаёшь один срез структур, где каждая строка — это отдельный тестовый случай: что на вход, что ожидаешь на выходе, и название, чтобы потом в логах не ебаться. Элегантно и сука эффективно. - Мокирование. Твой код не должен падать, потому что упала база. Поэтому все зависимости выносишь в интерфейсы. А в тесте вместо реальной базы подсовываешь мок, который этот интерфейс реализует и делает только то, что тебе нужно для теста. Чистая изоляция, как в боксе.
- Гонки данных (Race Condition). А вот это, ёпта, самая хитрая жопа в многопоточности. Два потока лезут в одну переменную, и кто первый — хрен знает. Go тебе в помощь: запускаешь тесты с флагом
-race, и он тебе подсветит, где у тебя эти опасные места. Обязательно делай. - Покрытие кода (Code Coverage). Чтобы не было стыдно перед коллегами.
go test -coverпокажет процент. А если хочешь красивую картинку, где именно не покрыто, тоgo test -coverprofile=coverage.out && go tool cover -html=coverage.out— откроет в браузере, и ты увидишь, где ты сэкономил на тестах, пидарас.
Пример, чтобы было понятно, как это выглядит вживую
Допустим, у тебя есть функция сложения, проще некуда:
// calculator.go
package main
func Sum(a, b int) int {
return a + b
}
Вот как для неё писать тест по-взрослому, с таблицей:
// calculator_test.go
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSum(t *testing.T) {
// Вот она, мать её, таблица. Всё красиво и в одном месте.
testCases := []struct {
name string // Как обозвать случай, чтобы в логах не "TestSum/0" было
a int // Первое слагаемое
b int // Второе слагаемое
want int // Что, блядь, должно получиться
}{
{"Два плюса", 2, 3, 5},
{"Плюс и минус", 5, -3, 2},
{"Два минуса", -1, -1, -2},
{"С нулём", 10, 0, 10},
}
// Пробегаемся по всей таблице
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { // Запускаем подтест с вменяемым именем
got := Sum(tc.a, tc.b) // Вызываем тестируемую функцию
assert.Equal(t, tc.want, got, "А сумма-то не сошлась, ёпта!") // Проверяем
})
}
}
Вот и вся магия. Ничего сложного, но делать надо. Иначе потом будешь как тот Герасим — молча страдать, когда продакшн упадёт в три часа ночи.