Назад к блогу
Разработка

Тестирование веб-приложений: от unit до e2e

Полное руководство по тестированию: unit тесты с Jest/Vitest, интеграционные тесты, e2e с Playwright, TDD подход.

5 января 2026 г.
14 мин чтения
87 просмотров
MOLOTILO

MOLOTILO DIGITAL

Тестирование веб-приложений: от unit до e2e

Пирамида тестирования

Эффективная стратегия тестирования строится на пирамиде: много unit тестов, меньше интеграционных, ещё меньше e2e. Это обеспечивает баланс между скоростью и покрытием кода.

  • Unit тесты — тестируют отдельные функции и компоненты
  • Интеграционные тесты — проверяют взаимодействие модулей
  • E2E тесты — имитируют действия пользователя

Unit тесты с Vitest

Vitest — современный фреймворк для тестирования, совместимый с Jest API:

// utils/validation.ts
export function validateEmail(email: string): boolean {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

export function formatPrice(price: number, currency = 'RUB'): string {
  return new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency
  }).format(price);
}

// utils/validation.test.ts
import { describe, it, expect } from 'vitest';
import { validateEmail, formatPrice } from './validation';

describe('validateEmail', () => {
  it('should return true for valid emails', () => {
    expect(validateEmail('test@example.com')).toBe(true);
    expect(validateEmail('user.name@domain.org')).toBe(true);
  });

  it('should return false for invalid emails', () => {
    expect(validateEmail('invalid')).toBe(false);
    expect(validateEmail('no@')).toBe(false);
    expect(validateEmail('@domain.com')).toBe(false);
  });
});

describe('formatPrice', () => {
  it('should format price in RUB by default', () => {
    expect(formatPrice(1000)).toContain('1');
    expect(formatPrice(1000)).toContain('000');
  });

  it('should format price in specified currency', () => {
    const result = formatPrice(100, 'USD');
    expect(result).toContain('$');
  });
});

Тестирование React компонентов

// components/Button.tsx
interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
  loading?: boolean;
}

export function Button({ children, onClick, disabled, loading }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled || loading}
      className="btn"
    >
      {loading ? 'Loading...' : children}
    </button>
  );
}

// components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';

describe('Button', () => {
  it('renders children correctly', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onClick when clicked', () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click</Button>);
    
    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('shows loading state', () => {
    render(<Button loading>Submit</Button>);
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  it('is disabled when loading', () => {
    render(<Button loading>Submit</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

Интеграционные тесты

// api/users.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createServer } from '../server';
import { prisma } from '../lib/prisma';

let server: any;

beforeAll(async () => {
  server = await createServer();
  await server.listen(0);
});

afterAll(async () => {
  await server.close();
  await prisma.$disconnect();
});

describe('Users API', () => {
  it('GET /api/users returns list of users', async () => {
    const response = await fetch(`http://localhost:${server.port}/api/users`);
    const data = await response.json();
    
    expect(response.status).toBe(200);
    expect(Array.isArray(data)).toBe(true);
  });

  it('POST /api/users creates new user', async () => {
    const response = await fetch(`http://localhost:${server.port}/api/users`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        name: 'Test User',
        email: 'test@example.com'
      })
    });
    
    const user = await response.json();
    
    expect(response.status).toBe(201);
    expect(user.name).toBe('Test User');
    expect(user.id).toBeDefined();
  });
});

E2E тесты с Playwright

Playwright — мощный инструмент для end-to-end тестирования:

// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Authentication', () => {
  test('user can login with valid credentials', async ({ page }) => {
    await page.goto('/login');
    
    await page.fill('[name="email"]', 'user@example.com');
    await page.fill('[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('h1')).toContainText('Dashboard');
  });

  test('shows error for invalid credentials', async ({ page }) => {
    await page.goto('/login');
    
    await page.fill('[name="email"]', 'wrong@example.com');
    await page.fill('[name="password"]', 'wrongpassword');
    await page.click('button[type="submit"]');
    
    await expect(page.locator('.error')).toContainText('Invalid credentials');
  });
});

test.describe('Shopping Cart', () => {
  test('user can add product to cart', async ({ page }) => {
    await page.goto('/products');
    
    await page.click('[data-testid="product-1"] button');
    
    await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');
  });
});

TDD — Test Driven Development

TDD — подход, при котором тесты пишутся до кода:

  1. Red — напишите падающий тест
  2. Green — напишите минимальный код для прохождения
  3. Refactor — улучшите код, сохраняя тесты зелёными
// 1. Red — пишем тест
it('should calculate discount', () => {
  expect(calculateDiscount(100, 10)).toBe(90);
});

// 2. Green — минимальная реализация
function calculateDiscount(price: number, percent: number): number {
  return price - (price * percent / 100);
}

// 3. Refactor — улучшаем
function calculateDiscount(price: number, percent: number): number {
  if (percent < 0 || percent > 100) {
    throw new Error('Invalid discount percent');
  }
  return price * (1 - percent / 100);
}

Покрытие кода

# Запуск тестов с покрытием
npm test -- --coverage

# Результат
--------------------|---------|----------|---------|---------|
File                | % Stmts | % Branch | % Funcs | % Lines |
--------------------|---------|----------|---------|---------|
All files           |   85.71 |    83.33 |   90.00 |   85.71 |
 validation.ts      |   100   |    100   |   100   |   100   |
 utils.ts           |   75.00 |    66.67 |   80.00 |   75.00 |
--------------------|---------|----------|---------|---------|

Заключение

Тестирование — инвестиция в качество и скорость разработки. Начните с unit тестов критичной бизнес-логики, добавьте e2e для ключевых сценариев. Автоматизируйте запуск тестов в CI/CD.

Код без тестов — это legacy код с первого дня. Пишите тесты, которые дают уверенность в рефакторинге.

ТестированиеJestVitestPlaywrightTDD

Понравилась статья?

Подпишитесь на наш блог, чтобы не пропустить новые материалы