GOAT Integration Tests¶
GOAT (Go Application Testing) — фреймворк для интеграционного тестирования Go-микросервисов.
Включение генерации тестов¶
Простой вариант¶
Расширенная конфигурация¶
applications:
- name: api
goat_tests_config:
enabled: true
binary_path: /tmp/my-custom-path
transport: [api, sys]
Генерируемые файлы¶
После запуска генератора в директории tests/ появятся:
| Файл | Описание |
|---|---|
psg_app_gen.go |
Инициализация тестового окружения |
psg_base_suite_gen.go |
Базовый test suite |
psg_config_gen.go |
Конфигурация сервиса |
psg_helpers_gen.go |
HTTP клиент, retry логика |
psg_init_gen.go |
Интерфейс инициализации |
psg_main_test.go |
Точка входа для тестов |
Требования¶
- Go 1.24+
- Docker (для testcontainers)
- Скомпилированный бинарник приложения
Настройка проекта¶
1. Создайте init.go¶
Это обязательный файл, реализующий интерфейс TestEnvInitializer:
package tests
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
gtt "github.com/Educentr/goat"
"github.com/Educentr/goat-services/psql"
"github.com/Educentr/goat/services"
"github.com/Educentr/goat/testutil"
)
type testEnvInitializerImpl struct{}
type myTestConfig struct {
*YourProjectConfig
}
func (c *myTestConfig) NewExecutor(env *gtt.Env, mockAddress string) *gtt.Executor {
envVars := c.loadEnvVars()
c.configureDB(envVars, env)
return gtt.NewExecutorBuilder(c.BinaryPath()).
WithEnv(envVars).
Build()
}
func (c *myTestConfig) loadEnvVars() map[string]string {
envVars, _ := testutil.LoadEnvFile("tests/etc/onlineconf/onlineconf.env")
envVars["ONLINECONFIG_FROM_ENV"] = "true"
return envVars
}
func (c *myTestConfig) configureDB(envVars map[string]string, env *gtt.Env) {
pg := services.MustGetTyped[*psql.Env](env.Manager(), "postgres")
svc := c.ServiceName()
envVars[fmt.Sprintf("OC_%s__db__main", svc)] = fmt.Sprintf("%s:%s", pg.DBHost, pg.DBPort)
envVars[fmt.Sprintf("OC_%s__db__main__User", svc)] = pg.DBUser
envVars[fmt.Sprintf("OC_%s__db__main__Password", svc)] = pg.DBPass
envVars[fmt.Sprintf("OC_%s__db__main__DB", svc)] = pg.DBName
}
func (c *myTestConfig) ApplyMigrations(ctx context.Context, db *sql.DB) error {
migrationsPath, _ := filepath.Abs(filepath.Join("..", "etc/database/postgres"))
migrationFiles := []string{"01_schema.sql"}
for _, file := range migrationFiles {
content, err := os.ReadFile(filepath.Join(migrationsPath, file))
if err != nil {
continue
}
if _, err := db.ExecContext(ctx, string(content)); err != nil {
return fmt.Errorf("apply migration %s: %w", file, err)
}
}
return nil
}
func (c *myTestConfig) CleanupTables(ctx context.Context, db *sql.DB) error {
tables := []string{"users", "sessions"}
for _, table := range tables {
if _, err := db.ExecContext(ctx, fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", table)); err != nil {
return fmt.Errorf("truncate %s: %w", table, err)
}
}
return nil
}
func (t *testEnvInitializerImpl) InitTestEnv() (testutil.TestAppConfig, *gtt.Env) {
config := &myTestConfig{YourProjectConfig: NewYourProjectConfig()}
services.MustRegisterServiceFuncTyped("postgres", psql.Run)
servicesMap := services.NewServicesMap("postgres")
manager := services.NewManager(servicesMap, services.DefaultManagerConfig())
env := gtt.NewEnv(gtt.EnvConfig{}, manager)
return config, env
}
func init() {
testEnvInit = &testEnvInitializerImpl{}
}
Написание тестов¶
Структура test suite¶
package tests
import (
"net/http"
"testing"
"github.com/stretchr/testify/suite"
)
type MyFeatureTestSuite struct {
BaseTestSuite
}
func (s *MyFeatureTestSuite) TestMyFeature() {
// 1. Подготовка данных
// 2. Выполнение запроса
resp, body := s.client.Get(s.T(), "/my/endpoint")
// 3. Проверка результата
s.Require().Equal(http.StatusOK, resp.StatusCode)
}
func TestMyFeatureTestSuite(t *testing.T) {
suite.Run(t, new(MyFeatureTestSuite))
}
HTTP клиент¶
// GET запрос
resp, body := s.client.Get(s.T(), "/endpoint")
// POST запрос
reqBody := map[string]interface{}{"field": "value"}
resp, body := s.client.Post(s.T(), "/endpoint", reqBody)
// С авторизацией
s.client.WithAuth(token)
resp, body := s.client.Get(s.T(), "/protected")
Методы BaseTestSuite¶
| Метод | Описание |
|---|---|
s.ctx |
Context для операций |
s.env |
GOAT Environment |
s.client |
HTTP клиент |
s.Mocks() |
Mock-серверы |
Mock-серверы для внешних API¶
Если приложение использует ogen_client:
package tests
import (
"context"
"net/http"
externalapi "github.com/your-org/your-project/pkg/rest/external/v1"
externalmock "github.com/your-org/your-project/pkg/mocks/external"
"go.uber.org/mock/gomock"
)
type MockServers struct {
ExternalHandler *externalmock.MockHandler
}
func HTTPMocksSetup(mocks *MockServers) func(server *http.ServeMux, ctl *gomock.Controller) {
return func(server *http.ServeMux, ctl *gomock.Controller) {
mocks.ExternalHandler = externalmock.NewMockHandler(ctl)
externalServer, _ := externalapi.NewServer(mocks.ExternalHandler, nil)
server.Handle("/external/", externalServer)
}
}
Использование в тестах¶
func (s *MyTestSuite) TestExternalAPICall() {
s.Mocks().ExternalHandler.EXPECT().
GetData(gomock.Any()).
Return(&externalapi.DataResponse{Status: "success"}, nil).
Times(1)
resp, body := s.client.Get(s.T(), "/my-endpoint")
s.Require().Equal(http.StatusOK, resp.StatusCode)
}
Запуск тестов¶
Через Makefile¶
make goat-tests # Сборка и запуск
make goat-tests-verbose # С подробным выводом
make build_for_test-api # Только сборка бинарника
Напрямую¶
# Сборка бинарника
CGO_ENABLED=1 go build -cover -race -o /tmp/api ./cmd/api
# Запуск тестов
go test -v ./tests/...
# Конкретный suite
go test -v -run TestAuthTestSuite ./tests/...
# Конкретный тест
go test -v -run TestAuthTestSuite/TestLogin ./tests/...
Структура файлов¶
your-project/
├── cmd/api/
│ └── main.go
├── tests/
│ ├── psg_*.go # [generated]
│ ├── init.go # [manual] Ваша реализация
│ ├── factories.go # [manual] Фабрики данных
│ ├── fixtures.go # [manual] Константы
│ ├── assertions.go # [manual] Assertion helpers
│ ├── auth_test.go # [manual] Ваши тесты
│ └── etc/onlineconf/ # Тестовая конфигурация
└── etc/database/postgres/ # SQL миграции
Best Practices¶
- Используйте фабрики для создания тестовых данных
- Используйте fixtures для констант
- Наследуйте BaseTestSuite
- Очищайте данные между тестами (CleanupTables)
- Понятные имена тестов:
TestFeature_WhenCondition_ExpectedResult
Troubleshooting¶
"API did not become ready"¶
- Проверьте бинарник:
ls -la /tmp/api - Docker запущен
- Порты не заняты
- Логи:
tail -f /tmp/yourproject-test-*.log
"TestEnvInitializer not implemented"¶
- Создайте
tests/init.go - Реализуйте интерфейс
- Присвойте в
init():testEnvInit = &testEnvInitializerImpl{}