add service
This commit is contained in:
309
TESTING.md
Normal file
309
TESTING.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# Руководство по тестированию
|
||||
|
||||
## Архитектура для тестирования
|
||||
|
||||
Проект спроектирован с учетом тестируемости:
|
||||
|
||||
### ✅ Интерфейсы для всех слоев
|
||||
|
||||
**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 автоматически проверит что все ожидания выполнены
|
||||
}
|
||||
```
|
||||
|
||||
### Преимущества 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")` прямо в тестах.
|
||||
Reference in New Issue
Block a user