Тесты в подпапках Go: решаем проблему, о которой молчат туториалы
Вы когда-нибудь писали тесты в Go и сталкивались с тем, что они отлично работают в корне проекта, но стоит перенести их в подпапку — всё ломается? Знакомо? Если да — вы не одиноки. Вопрос "Golang tests in sub-directory" на Stack Overflow собрал тысячи просмотров. И это неудивительно: документация Go говорит об этом сухо, а начинающие разработчики часто теряются.
В этой статье мы разберёмся, как правильно организовать тестирование Go с использованием вложенных пакетов, зачем это нужно и как не наступить на грабли. Поехали!
Почему тесты в подпапках — это боль?
Представьте: у вас есть проект, структура которого выглядит так:
myapp/
├── main.go
├── go.mod
└── utils/
├── math.go
└── math_test.go
Вы пишете Go test в корне — и всё работает. Но когда вы пытаетесь запустить тесты из папки utils, Go ругается: "package myapp/utils is not in GOROOT". Знакомая ошибка?
Проблема в том, что Go работает с пакетами, а не с папками. Когда вы создаёте подпапку, вы фактически создаёте новый пакет. И тесты в нём нужно запускать с учётом модульной структуры проекта. Но давайте по порядку.
Основы: как Go видит тесты
В Go тесты — это обычные файлы с суффиксом _test.go. Они могут находиться:
- В том же пакете (белые тесты) — имеют доступ к неэкспортированным функциям.
- В отдельном пакете с суффиксом
_test(чёрные тесты) — тестируют пакет как внешний пользователь.
Когда вы помещаете тесты в подпапку, вы создаёте новый пакет. Например, папка utils — это пакет utils. И тесты в ней должны принадлежать этому пакету.
Пример правильной структуры
myapp/
├── go.mod
├── main.go
└── utils/
├── math.go // package utils
└── math_test.go // package utils
В файле math.go:
package utils
func Add(a, b int) int {
return a + b
}
В файле math_test.go:
package utils
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Expected 5, got %d", result)
}
}
Теперь запустите тесты из корня проекта:
go test ./utils/
Или из папки utils:
cd utils
go test
Всё работает! Но что если у вас более сложная структура?
Вложенные подпапки: глубже, чем кажется
Допустим, у вас есть проект с такой структурой:
myapp/
├── go.mod
├── main.go
└── services/
├── user/
│ ├── user.go
│ └── user_test.go
└── order/
├── order.go
└── order_test.go
Здесь каждая подпапка — отдельный пакет. Чтобы запустить все тесты рекурсивно, используйте три точки:
go test ./...
Эта команда найдёт все пакеты в проекте и запустит их тесты. Очень удобно для CI/CD!
Что если тесты в той же папке, что и код?
Иногда разработчики кладут тесты в ту же папку, что и основной код. Это нормально для небольших проектов. Но для крупных проектов лучше выносить тесты в отдельные подпапки — так структура становится чище.
Типичные ошибки и их решения
Давайте разберём самые частые проблемы, с которыми сталкиваются новички при работе с Golang tests in sub-directory.
Ошибка 1: "package is not in GOROOT"
Причина: Вы пытаетесь запустить тест из подпапки, не указав модуль. Go не знает, к какому модулю относится пакет.
Решение: Убедитесь, что у вас есть go.mod в корне проекта. И запускайте тесты через go test ./папка/ из корня.
Ошибка 2: циклические зависимости
Причина: Пакет A импортирует пакет B, а пакет B импортирует пакет A. В Go такое запрещено.
Решение: Пересмотрите архитектуру. Вынесите общие типы в отдельный пакет.
Ошибка 3: тесты не видят функции из родительского пакета
Причина: Подпапка — это отдельный пакет. Функции из родительского пакета нужно импортировать.
Решение: Используйте импорт по полному пути модуля:
import "myapp/utils"
Best practices для организации тестов
Опираясь на опыт сообщества и документацию Go, вот несколько советов, которые помогут вам писать чистые и поддерживаемые тесты:
- Используйте
go test ./...для запуска всех тестов — это стандарт в Go-проектах. - Называйте тестовые файлы осмысленно:
user_test.go,order_service_test.go. - Группируйте тесты по функциональности: если пакет большой, разбейте тесты на несколько файлов.
- Используйте
t.Run()для подтестов — это улучшает читаемость и позволяет запускать отдельные кейсы. - Не бойтесь выносить тестовые данные в отдельные файлы — например,
testdata/.
Пример с подтестами
package utils
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 2, 3, 5},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("Add() = %d, want %d", got, tt.want)
}
})
}
}
Продвинутые техники: тесты с зависимостями
В реальных проектах код часто зависит от баз данных, API или файлов. Как тестировать такие