# 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