# Decision Record: Test Strategy
**Дата**: 2026-06-11
**Статус**: Accepted (требует реализации)
## Контекст
В проекте **0% тестов**. Ни unit, ни integration, ни e2e. Это блокер для безопасного рефакторинга и CI/CD.
## Решение
**Трёхслойная стратегия тестирования** с целевым покрытием:
| Слой | Цель покрытия | Инструменты | Что тестируем |
|------|---------------|-------------|---------------|
| **Unit** | 70%+ | Go: `testing`, `testify`, `gomock` / `go.uber.org/mock`
TS: `vitest`, `react-testing-library` | Бизнес-логика (services), утилиты, валидаторы, мапперы |
| **Integration** | Критичные пути | Go: `testcontainers-go` (PostgreSQL, Redis, MinIO)
TS: `msw` (Mock Service Worker) | Repository слой, HTTP handlers, API контракты, auth flow |
| **E2E** | Happy paths | `Playwright` (TypeScript) | Critical user journeys: регистрация → создание места → бронирование → оплата |
## Backend: Unit тесты (services)
```go
// internal/services/auth_service_test.go
func TestAuthService_Register(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := repository.NewMockUserRepo(ctrl)
svc := services.NewAuthService(mockRepo, "secret", "refresh-secret")
mockRepo.EXPECT().GetByEmail(gomock.Any(), "test@example.com").Return(nil, nil)
mockRepo.EXPECT().Create(gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, u *models.User) error {
u.ID = "uuid-123"
return nil
}).Times(1)
result, err := svc.Register(context.Background(), services.RegisterInput{
Email: "test@example.com", Password: "pass123", Role: "customer", Name: "Test",
})
assert.NoError(t, err)
assert.NotEmpty(t, result.AccessToken)
assert.NotEmpty(t, result.RefreshToken)
assert.Equal(t, "customer", result.User.Role)
}
```
## Backend: Integration тесты (handlers + DB)
```go
// internal/handlers/places_integration_test.go
func TestPlaceHandler_Create(t *testing.T) {
// testcontainers: запустить postgres
ctx := context.Background()
pgContainer, _ := postgres.RunContainer(ctx, testcontainers.WithImage("postgis/postgis:16-3.4-alpine"))
defer pgContainer.Terminate(ctx)
// Накатить миграции
dbURL := pgContainer.ConnectionString(ctx)
migrate.Up(dbURL)
// Создать реальный pool, repo, service, handler
pool := repository.NewPool(ctx, dbURL)
placeRepo := repository.NewPlaceRepo(pool)
placeSvc := services.NewPlaceService(placeRepo)
handler := handlers.NewPlaceHandler(placeSvc)
// Сделать HTTP запрос через httptest
req := httptest.NewRequest("POST", "/api/v1/places", strings.NewReader(`{"title":"Test","type":"place","lat":55.75,"lng":37.61}`))
req.Header.Set("Authorization", "Bearer "+generateTestToken("user-1", "customer"))
w := httptest.NewRecorder()
handler.Create(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
}
```
## Frontend: Unit тесты (components, hooks, utils)
```typescript
// src/hooks/__tests__/useAuth.test.tsx
import { renderHook, act } from '@testing-library/react'
import { AuthProvider, useAuth } from '@/hooks/useAuth'
import { api } from '@/lib/api'
vi.mock('@/lib/api')
test('login sets user and token', async () => {
const mockUser = { id: '1', email: 'test@test.com', role: 'customer' }
vi.mocked(api.post).mockResolvedValue({ user: mockUser, access_token: 'token123' })
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useAuth(), { wrapper })
await act(async () => {
await result.current.login('test@test.com', 'password')
})
expect(result.current.user).toEqual(mockUser)
expect(localStorage.getItem('access_token')).toBe('token123')
})
```
## E2E: Playwright critical paths
```typescript
// e2e/booking.spec.ts
test('user can book a studio', async ({ page }) => {
await page.goto('/login')
await page.fill('[name=email]', 'user@test.com')
await page.fill('[name=password]', 'password')
await page.click('button[type=submit]')
await page.goto('/studios/studio-123')
await page.click('[data-testid=book-button]')
await page.fill('[name=start_time]', '2024-02-01T10:00')
await page.fill('[name=end_time]', '2024-02-01T14:00')
await page.click('button[type=submit]')
await expect(page.locator('[data-testid=booking-confirmation]')).toBeVisible()
})
```
## CI Pipeline
```yaml
# .github/workflows/ci.yml
jobs:
backend-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgis/postgis:16-3.4-alpine
env: { POSTGRES_DB: test, POSTGRES_USER: test, POSTGRES_PASSWORD: test }
ports: [5432:5432]
options: >-
--health-cmd "pg_isready -U test"
--health-interval 5s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports: [6379:6379]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: '1.22' }
- run: go test ./backend/... -race -coverprofile=coverage.out
- uses: codecov/codecov-action@v4
frontend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm', cache-dependency-path: frontend/package-lock.json }
- run: cd frontend && npm ci && npm run test:ci
e2e-test:
runs-on: ubuntu-latest
needs: [backend-test, frontend-test]
steps:
- uses: actions/checkout@v4
- run: docker compose -f deploy/docker-compose.prod.yml up -d
- run: npx playwright install --with-deps chromium
- run: npx playwright test
```
## Последствия
**Положительные:**
- Безопасный рефакторинг
- Документация поведения через тесты
- CI как quality gate
- Регрессии ловятся автоматически
**Отрицательные:**
- Время на написание (инвестиция)
- Поддержка тестов при изменениях
- testcontainers медленные в CI (нужен кэш)
## Связанные заметки
- [[backend-auth-security]] — тесты для refresh token rotation
- [[backend-validation]] — тесты для кастомных валидаторов
- [[architecture-overview]]
## Теги
#decision-record #testing #ci-cd #unit #integration #e2e #best-practice