All checks were successful
Deploy Smart Search Backend Test / deploy (push) Successful in 1m24s
346 lines
11 KiB
Markdown
346 lines
11 KiB
Markdown
# Руководство по тестированию
|
||
|
||
## Архитектура для тестирования
|
||
|
||
Проект спроектирован с учетом тестируемости:
|
||
|
||
### ✅ Интерфейсы для всех слоев
|
||
|
||
**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")` прямо в тестах.
|