# Руководство по тестированию ## Архитектура для тестирования Проект спроектирован с учетом тестируемости: ### ✅ Интерфейсы для всех слоев **Repository интерфейсы** (`internal/repository/interfaces.go`): ```go type UserRepository interface { FindByEmailHash(ctx context.Context, emailHash string) (*model.User, error) FindByID(ctx context.Context, userID int) (*model.User, error) Create(ctx context.Context, user *model.User) error // ... } ``` **Service интерфейсы** (`internal/service/interfaces.go`): ```go type AuthService interface { Login(ctx context.Context, email, password, ip, userAgent string) (string, string, error) Refresh(ctx context.Context, refreshToken string) (string, error) // ... } ``` ### ✅ Context пробрасывается через все слои ``` main.go (ctx) → Workers (ctx) → gRPC Handler (ctx) → Service (ctx) → Repository (ctx) → pgx ``` **Правило**: `context.Background()` создается **только один раз** в `main.go` и прокидывается через все компоненты. Это позволяет: - **Graceful shutdown**: при отмене context все workers и операции останавливаются - Отменять долгие операции - Передавать метаданные (trace ID, user ID) - Контролировать таймауты - Избежать потерянных goroutines ## Автоматическая генерация моков Проект использует [minimock](https://github.com/gojuno/minimock) для автоматической генерации типизированных моков из интерфейсов. ### Генерация моков Все моки генерируются в `internal/mocks/` одной командой: ```bash make generate-mock ``` Эта команда генерирует моки для всех интерфейсов: - **Repository интерфейсы**: `UserRepository`, `SessionRepository`, `InviteRepository`, `RequestRepository`, `SupplierRepository`, `TokenUsageRepository` - **Service интерфейсы**: `AuthService`, `UserService`, `InviteService`, `RequestService`, `SupplierService` ### Использование сгенерированных моков ```go import ( "testing" "context" "smart-search-back/internal/mocks" "smart-search-back/internal/service" "smart-search-back/internal/model" "github.com/stretchr/testify/assert" ) func TestAuthService_Login_Success(t *testing.T) { // Создаем моки mockUserRepo := mocks.NewUserRepositoryMock(t) mockSessionRepo := mocks.NewSessionRepositoryMock(t) // Настраиваем поведение мока mockUserRepo.FindByEmailHashMock.Expect(context.Background(), "email_hash").Return(&model.User{ ID: 1, PasswordHash: "b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86", }, nil) mockSessionRepo.CreateMock.Expect(context.Background(), &model.Session{}).Return(nil) // Создаем сервис с моками authService := service.NewAuthService(mockUserRepo, mockSessionRepo) // Выполняем тест accessToken, refreshToken, err := authService.Login( context.Background(), "test@example.com", "password", "127.0.0.1", "test-agent", ) // Проверяем результат assert.NoError(t, err) assert.NotEmpty(t, accessToken) assert.NotEmpty(t, refreshToken) // Minimock автоматически проверит что все ожидания выполнены } func TestAuthService_Register_Success(t *testing.T) { mockUserRepo := mocks.NewUserRepositoryMock(t) mockSessionRepo := mocks.NewSessionRepositoryMock(t) mockInviteRepo := mocks.NewInviteRepositoryMock(t) mockInviteRepo.FindActiveByCodeMock.Expect(context.Background(), int64(123456)).Return(&model.InviteCode{ Code: 123456, CanBeUsedCount: 5, UsedCount: 0, }, nil) mockUserRepo.FindByEmailHashMock.Expect(context.Background(), "email_hash").Return(nil, errors.NewBusinessError(errors.UserNotFound, "user not found")) mockUserRepo.CreateTxMock.Return(nil) mockInviteRepo.DecrementCanBeUsedCountTxMock.Return(nil) mockSessionRepo.CreateMock.Return(nil) authService := service.NewAuthService(mockUserRepo, mockSessionRepo, mockInviteRepo, txManager, "secret", "cryptosecret") accessToken, refreshToken, err := authService.Register( context.Background(), "newuser@example.com", "password123", "New User", "+1234567890", 123456, "127.0.0.1", "test-agent", ) assert.NoError(t, err) assert.NotEmpty(t, accessToken) assert.NotEmpty(t, refreshToken) } ``` ### Преимущества minimock ✅ **Типобезопасность** - моки генерируются из интерфейсов, ошибки компиляции при изменении сигнатур ✅ **Автоматическая проверка** - проверяет что все ожидания выполнены ✅ **Счетчики вызовов** - можно проверить сколько раз был вызван метод ✅ **Inspection** - можно проверить аргументы вызовов ✅ **Minimal boilerplate** - не нужно писать моки вручную ### Пример с проверкой вызовов ```go func TestUserService_GetBalance(t *testing.T) { mockUserRepo := mocks.NewUserRepositoryMock(t) // Ожидаем вызов GetBalance с userID=123 mockUserRepo.GetBalanceMock.Expect(context.Background(), 123).Return(100.50, nil) userService := service.NewUserService(mockUserRepo, nil) balance, err := userService.GetBalance(context.Background(), 123) assert.NoError(t, err) assert.Equal(t, 100.50, balance) // Minimock автоматически проверит: // - что GetBalance был вызван ровно 1 раз // - с правильными аргументами // - и вернул правильное значение } ``` ### Ручные моки (legacy пример) Для сравнения, старый пример с ручными моками (все еще работает, но не рекомендуется): ```go // Mock репозитория (legacy - используйте minimock!) type mockUserRepo struct { findByEmailHashFunc func(ctx context.Context, emailHash string) (*model.User, error) } func (m *mockUserRepo) FindByEmailHash(ctx context.Context, emailHash string) (*model.User, error) { if m.findByEmailHashFunc != nil { return m.findByEmailHashFunc(ctx, emailHash) } return nil, nil } ``` ## Запуск тестов ```bash # Все тесты go test ./... # С покрытием go test ./... -cover # Verbose режим go test ./... -v # Конкретный пакет go test ./internal/service/tests/... # С race detector go test ./... -race ``` ## Использование minimock Проект использует [minimock](https://github.com/gojuno/minimock) для автоматической генерации типизированных моков. ### Генерация моков Все моки генерируются в `internal/mocks/`: ```bash make generate-mock ``` Генерируются моки для всех интерфейсов: - **Repository**: `UserRepository`, `SessionRepository`, `InviteRepository`, `RequestRepository`, `SupplierRepository`, `TokenUsageRepository` - **Service**: `AuthService`, `UserService`, `InviteService`, `RequestService`, `SupplierService` ### Использование сгенерированных моков ```go import "smart-search-back/internal/mocks" func TestAuthService_Login(t *testing.T) { mockUserRepo := mocks.NewUserRepositoryMock(t) mockSessionRepo := mocks.NewSessionRepositoryMock(t) mockUserRepo.FindByEmailHashMock.Expect(ctx, "hash").Return(&model.User{...}, nil) authService := service.NewAuthService(mockUserRepo, mockSessionRepo) // ... тест } ``` Подробнее см. раздел "Автоматическая генерация моков" выше. ## Структура тестов ``` internal/ ├── service/ │ ├── auth.go │ ├── interfaces.go # Интерфейсы сервисов │ └── tests/ │ └── auth_test.go # Тесты с моками ├── repository/ │ ├── user.go │ ├── interfaces.go # Интерфейсы репозиториев │ └── tests/ │ └── user_test.go ``` ## Best Practices ### 1. Используйте context.Background() в тестах ```go ctx := context.Background() result, err := service.SomeMethod(ctx, params) ``` ### 2. Мокайте только то, что нужно ```go mockRepo := &mockUserRepo{ findByIDFunc: func(ctx context.Context, id int) (*model.User, error) { return &model.User{ID: id}, nil }, // Остальные методы можно не реализовывать если не используются } ``` ### 3. Проверяйте вызовы ```go var called bool mockRepo := &mockUserRepo{ createFunc: func(ctx context.Context, user *model.User) error { called = true assert.Equal(t, "expected@email.com", user.Email) return nil }, } // ... вызов сервиса assert.True(t, called, "Create should have been called") ``` ### 4. Тестируйте ошибки ```go mockRepo := &mockUserRepo{ findByIDFunc: func(ctx context.Context, id int) (*model.User, error) { return nil, errors.NewBusinessError(errors.UserNotFound, "user not found") }, } result, err := service.GetUser(ctx, 123) assert.Error(t, err) assert.Nil(t, result) ``` ## Integration тесты Для integration тестов с реальной БД можно использовать testcontainers: ```go func TestUserRepository_Integration(t *testing.T) { // Создать testcontainer с PostgreSQL // Применить миграции // Запустить тесты } ``` ## CI/CD ```yaml # .github/workflows/test.yml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: go-version: 1.21 - run: go test ./... -race -cover ``` ## Хэши для тестов При тестировании аутентификации используйте правильные хэши: - Пароль: `"password"` - SHA512 хэш: `"b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86"` Или используйте `crypto.PasswordHash("password")` прямо в тестах.