Стандарты
Тестирование
Требования к тестированию fullstack-проектов и модулей.
Требования к тестированию fullstack-проектов и модулей.
Обязательный стек
| Инструмент | Назначение | Уровень |
|---|---|---|
| Vitest | Unit-тесты | Проект + Модули |
| Storybook | Документация компонентов | Проект + UI layer |
| Chromatic | Visual regression | Проект + UI layer |
| Cypress | E2E тесты | Проект |
| Artillery | Нагрузочные тесты | Проект |
| Unlighthouse | Performance regression | Проект |
| Sentry | Error monitoring | Проект |
Vitest (Unit)
Что тестировать
| Обязательно | Желательно |
|---|---|
| Composables | Page components |
| Utils/helpers | Complex components |
| Store actions | Edge cases |
| Чистые функции |
Структура тестов
app/
├── composables/
│ ├── useOrders.ts
│ └── useOrders.test.ts # Рядом с файлом
├── utils/
│ ├── formatCurrency.ts
│ └── formatCurrency.test.ts
└── stores/
├── user.ts
└── user.test.ts
Пример теста
// useOrders.test.ts
import { describe, it, expect, vi } from 'vitest'
import { useOrders } from './useOrders'
describe('useOrders', () => {
it('fetches orders list', async () => {
const { orders, fetchOrders } = useOrders()
await fetchOrders()
expect(orders.value).toHaveLength(10)
})
it('handles error', async () => {
vi.mocked(useAuthFetch).mockRejectedValue(new Error('Network error'))
const { error, fetchOrders } = useOrders()
await fetchOrders()
expect(error.value).toBe('Network error')
})
})
Конфигурация
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom',
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: ['node_modules', 'tests'],
},
include: ['app/**/*.test.ts'],
},
})
Coverage пороги
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
thresholds: {
statements: 60,
branches: 60,
functions: 60,
lines: 60,
},
},
},
})
Storybook + Chromatic (Component)
Что документировать
| Обязательно | Желательно |
|---|---|
| UI layer компоненты | Domain components |
| Shared компоненты | Layout components |
| Компоненты с состояниями |
Структура stories
app/components/
├── OrderCard/
│ ├── OrderCard.vue
│ └── OrderCard.stories.ts
└── ui/
├── Button/
│ ├── Button.vue
│ └── Button.stories.ts
Пример story
// OrderCard.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import OrderCard from './OrderCard.vue'
const meta: Meta<typeof OrderCard> = {
title: 'Domain/OrderCard',
component: OrderCard,
tags: ['autodocs'],
argTypes: {
status: {
control: 'select',
options: ['pending', 'processing', 'completed', 'cancelled'],
},
},
}
export default meta
type Story = StoryObj<typeof OrderCard>
export const Default: Story = {
args: {
order: {
id: '123',
status: 'pending',
total: 15000,
createdAt: '2025-01-23',
},
},
}
export const Processing: Story = {
args: {
order: {
id: '124',
status: 'processing',
total: 25000,
createdAt: '2025-01-22',
},
},
}
export const Completed: Story = {
args: {
order: {
id: '125',
status: 'completed',
total: 30000,
createdAt: '2025-01-21',
},
},
}
Chromatic интеграция
# Установка
pnpm add -D chromatic
# Запуск
pnpm chromatic --project-token=<token>
CI интеграция:
chromatic:
script:
- pnpm storybook:build
- pnpm chromatic --project-token=$CHROMATIC_TOKEN --exit-zero-on-changes
Cypress (E2E)
Что тестировать
| Обязательно | Желательно |
|---|---|
| Авторизация | Навигация |
| Критические формы | Фильтрация |
| Основные CRUD операции | Pagination |
| Оплата (если есть) |
Структура тестов
tests/
└── e2e/
├── auth/
│ ├── login.cy.ts
│ └── logout.cy.ts
├── orders/
│ ├── create.cy.ts
│ ├── list.cy.ts
│ └── details.cy.ts
└── support/
├── commands.ts
└── e2e.ts
Пример теста
// auth/login.cy.ts
describe('Login', () => {
beforeEach(() => {
cy.visit('/login')
})
it('logs in with valid credentials', () => {
cy.get('[data-testid="email"]').type('admin@example.com')
cy.get('[data-testid="password"]').type('password123')
cy.get('[data-testid="submit"]').click()
cy.url().should('eq', Cypress.config('baseUrl') + '/')
cy.get('[data-testid="user-menu"]').should('be.visible')
})
it('shows error for invalid credentials', () => {
cy.get('[data-testid="email"]').type('wrong@example.com')
cy.get('[data-testid="password"]').type('wrongpassword')
cy.get('[data-testid="submit"]').click()
cy.get('[data-testid="error-message"]').should('contain', 'Invalid credentials')
})
})
Cypress Studio
Использовать Cypress Studio для записи тестов:
// cypress.config.ts
export default defineConfig({
e2e: {
experimentalStudio: true,
},
})
Custom Commands
// support/commands.ts
Cypress.Commands.add('login', (email: string, password: string) => {
cy.session([email, password], () => {
cy.visit('/login')
cy.get('[data-testid="email"]').type(email)
cy.get('[data-testid="password"]').type(password)
cy.get('[data-testid="submit"]').click()
cy.url().should('not.include', '/login')
})
})
Artillery (Load)
Когда использовать
- Перед релизом major features
- После оптимизаций
- Регулярно (nightly builds)
Конфигурация
# tests/load/api-load.yml
config:
target: "https://api.example.com"
phases:
- duration: 60
arrivalRate: 10
name: "Warm up"
- duration: 120
arrivalRate: 50
name: "Sustained load"
- duration: 60
arrivalRate: 100
name: "Peak load"
scenarios:
- name: "Browse orders"
flow:
- get:
url: "/api/orders"
headers:
Authorization: "Bearer $processEnvironment.TEST_TOKEN"
- think: 2
- get:
url: "/api/orders/$randomNumber(1, 1000)"
Пороги
config:
ensure:
p95: 500 # 95th percentile < 500ms
maxErrorRate: 1 # Error rate < 1%
Unlighthouse (Performance)
Конфигурация
// unlighthouse.config.ts
export default {
site: 'https://app.example.com',
scanner: {
device: 'desktop',
throttle: true,
},
ci: {
budget: {
performance: 80,
accessibility: 90,
'best-practices': 80,
seo: 80,
},
},
}
CI интеграция
performance:
script:
- pnpm unlighthouse-ci --site https://staging.example.com
artifacts:
paths:
- .unlighthouse/
Sentry (Errors)
Настройка
pnpm add @sentry/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@sentry/nuxt/module'],
sentry: {
dsn: process.env.SENTRY_DSN,
},
sourcemap: {
client: true,
},
})
Ручной отлов ошибок
// В composable или component
import * as Sentry from '@sentry/nuxt'
try {
await riskyOperation()
} catch (error) {
Sentry.captureException(error, {
tags: {
module: 'orders',
action: 'create',
},
extra: {
orderId: order.id,
},
})
}
Breadcrumbs
Sentry.addBreadcrumb({
category: 'navigation',
message: `User navigated to ${route.path}`,
level: 'info',
})
User Context
// При авторизации
Sentry.setUser({
id: user.id,
email: user.email,
username: user.name,
})
// При выходе
Sentry.setUser(null)
Scripts в package.json
{
"scripts": {
"test": "vitest",
"test:unit": "vitest run",
"test:coverage": "vitest run --coverage",
"test:e2e": "cypress run",
"test:e2e:open": "cypress open",
"test:load": "artillery run tests/load/api-load.yml",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build",
"chromatic": "chromatic --exit-zero-on-changes",
"unlighthouse": "unlighthouse-ci"
}
}
Чек-лист
При создании проекта
- Настроен Vitest
- Настроен Storybook
- Настроен Cypress
- Настроен Sentry
- CI pipeline включает тесты
При создании компонента
- Unit-тест для логики (если есть)
- Story в Storybook (для shared)
- data-testid для E2E
При создании composable
- Unit-тесты для основных сценариев
- Тест для error handling
Code Review
- Новая логика покрыта тестами
- Stories обновлены (если UI изменился)
- data-testid добавлены (для новых элементов)