Правильное именование пакетов для тестирования в Go (unit-testing)
Основные подходы к именованию тестовых пакетов в Go
В Go существует два основных подхода к именованию пакетов для тестов: внутреннее тестирование (package foo) и внешнее тестирование (package foo_test). Оба варианта легитимны, но у них разные цели и ограничения.
1. Внутреннее тестирование (package foo)
Когда вы используете package calculator в файле calc_test.go, вы пишете тесты внутри того же пакета. Это даёт доступ ко всем объявленным в пакете сущностям — как экспортированным, так и неэкспортированным.
// internal/calculator/calc.go
package calculator
func Add(a, b int) int {
return a + b
}
// Неэкспортированная функция
func addInternal(a, b int) int {
return a + b + 1 // с ошибкой для демонстрации
}
// internal/calculator/calc_test.go
package calculator
import "testing"
func TestAddInternal(t *testing.T) {
result := addInternal(2, 3) // доступ к неэкспортированной функции
if result != 5 {
t.Errorf("addInternal(2,3) = %d; want 5", result)
}
}
Плюсы:
- Доступ к неэкспортированным функциям и переменным — можно тестировать внутреннюю логику.
- Меньше дублирования кода (не нужно создавать дополнительные обёртки).
- Подходит для модульного тестирования "белого ящика".
Минусы:
- Тесты становятся хрупкими — при изменении внутренней реализации тесты могут сломаться.
- Нарушается инкапсуляция: тесты "знают" о внутренностях пакета.
2. Внешнее тестирование (package foo_test)
Использование package calculator_test создаёт отдельный пакет для тестов. В этом случае вы можете обращаться только к экспортированным идентификаторам (с большой буквы).
// internal/calculator/calc_test.go
package calculator_test
import (
"testing"
"myapp/internal/calculator"
)
func TestAdd(t *testing.T) {
result := calculator.Add(2, 3)
if result != 5 {
t.Errorf("Add(2,3) = %d; want 5", result)
}
}
// Ошибка! addInternal не экспортирована:
// func TestAddInternal(t *testing.T) {
// result := calculator.addInternal(2, 3)
// ...
// }
Плюсы:
- Тесты проверяют только публичный API — это соответствует принципу "тестируйте поведение, а не реализацию".
- Тесты менее хрупкие: вы можете переписать внутренности пакета, и тесты не сломаются, если публичный интерфейс не изменился.
- Имитирует использование пакета сторонним кодом — выявляет проблемы с API.
Минусы:
- Нельзя напрямую тестировать неэкспортированные функции.
- Требуется импортировать тестируемый пакет (как это делают пользователи).
Когда что использовать?
- package foo — для модульных тестов, когда нужно протестировать внутреннюю логику, сложные алгоритмы или функции, которые не должны быть публичными. Особенно полезно для TDD (Test-Driven Development).
- package foo_test — для интеграционных или acceptance-тестов, когда вы проверяете, что публичный API работает корректно. Рекомендуется для библиотек, которые будут использоваться другими разработчиками.
- Комбинированный подход — можно использовать оба варианта в одном проекте. Например, в
calc_internal_test.go(package calculator) тестировать внутренние функции, а вcalc_test.go(package calculator_test) — публичный API.
Best practices
- Используйте
package foo_testкак основной подход — это стандарт Go сообщества и рекомендуется в официальной документации. - Если вам нужно протестировать неэкспортированную функцию, подумайте: а нужно ли её тестировать? Возможно, её поведение покрывается тестами публичных функций.
- Для сложных пакетов используйте отдельные файлы с суффиксом
_internal_test.goдля внутренних тестов — это явно показывает намерение. - Не бойтесь экспортировать функции, если они нужны для тестирования — это лучше, чем нарушать инкапсуляцию через внутренние тесты.
В итоге, для вашего примера с addInternal правильным решением будет либо сделать эту функцию экспортированной (если она полезна вне пакета), либо оставить внутренний тест (package calculator) для её проверки, а для публичного API использовать внешний тест (package calculator_test).
Альтернативный подход: тестирование через экспорт с помощью файлов-обёрток
Если вы хотите сохранить внешнее тестирование (package foo_test), но при этом протестировать неэкспортированные функции, можно использовать трюк с отдельным файлом, который экспортирует внутренние функции только для тестов.
// internal/calculator/calc_exports_test.go
// Этот файл компилируется только при тестировании
package calculator
// ExportedAddInternal — обёртка для тестов
var ExportedAddInternal = addInternal
Теперь в вашем внешнем тесте (package calculator_test) вы можете обратиться к этой функции:
// internal/calculator/calc_test.go
package calculator_test
import (
"testing"
"myapp/internal/calculator"
)
func TestAddInternalViaExport(t *testing.T) {
result := calculator.ExportedAddInternal(2, 3)
if result != 5 {
t.Errorf("addInternal(2,3) = %d; want 5", result)
}
}
Плюсы этого подхода:
- Вы сохраняете внешнее тестирование (package foo_test) со всеми его преимуществами.
- Неэкспортированные функции остаются скрытыми от пользователей пакета (файл
_test.goне компилируется в обычной сборке). - Явно указываете, какие внутренние функции тестируются.
Минусы:
- Дополнительный файл для каждого набора внутренних функций.
- Может ввести в заблуждение, если кто-то увидит "экспортированную" версию функции.
- Не является стандартным подходом — используйте его только при крайней необходимости.
Этот метод полезен, когда вы пишете библиотеку и хотите, чтобы тесты были максимально похожи на код пользователей, но при этом нужно проверить внутренние алгоритмы. Однако я рекомендую сначала попробовать подход с внутренними тестами (package foo) — он проще и понятнее для большинства случаев.
Ответов пока нет.
Войдите чтобы оставить ответ