🛡️ Искусство Тестирования
Защити свой код от багов и регрессий
"Тестирование показывает наличие багов, но не их отсутствие. Однако хорошие тесты — лучшая защита кода."
Пирамида Тестирования
🎭 E2E Тесты
Самые медленные, но самые реалистичные
🔗 Интеграционные Тесты
Проверяют взаимодействие компонентов
⚡ Юнит-Тесты
Быстрые и изолированные тесты функций
Типы Тестов
⚡ 1. Юнит-Тесты (Unit Tests)
Тестируют отдельные функции или методы в изоляции. Должны быть быстрыми и независимыми.
Jest / JavaScript
// Функция для тестирования
function calculateLightsaberPower(crystalPower, userLevel) {
if (crystalPower <= 0 || userLevel <= 0) {
throw new Error('Недопустимые значения');
}
return crystalPower * userLevel * 1.5;
}
// Тесты
describe('calculateLightsaberPower', () => {
test('правильно вычисляет силу', () => {
expect(calculateLightsaberPower(10, 5)).toBe(75);
});
test('выбрасывает ошибку при нулевой силе кристалла', () => {
expect(() => {
calculateLightsaberPower(0, 5);
}).toThrow('Недопустимые значения');
});
test('выбрасывает ошибку при отрицательном уровне', () => {
expect(() => {
calculateLightsaberPower(10, -1);
}).toThrow('Недопустимые значения');
});
test('работает с дробными числами', () => {
expect(calculateLightsaberPower(7.5, 4)).toBe(45);
});
});
🔗 2. Интеграционные Тесты
Проверяют работу нескольких компонентов вместе.
Jest / JavaScript
class JediDatabase {
constructor() {
this.jedis = [];
}
addJedi(jedi) {
if (!jedi.name || !jedi.rank) {
throw new Error('Неполные данные джедая');
}
this.jedis.push(jedi);
return jedi;
}
findJediByName(name) {
return this.jedis.find(j => j.name === name);
}
}
class JediService {
constructor(database) {
this.db = database;
}
registerJedi(name, rank) {
const jedi = { name, rank, registeredAt: new Date() };
return this.db.addJedi(jedi);
}
getJediInfo(name) {
const jedi = this.db.findJediByName(name);
if (!jedi) {
throw new Error('Джедай не найден');
}
return `${jedi.name} - ${jedi.rank}`;
}
}
// Интеграционный тест
describe('JediService с JediDatabase', () => {
let database;
let service;
beforeEach(() => {
database = new JediDatabase();
service = new JediService(database);
});
test('регистрация и получение информации о джедае', () => {
// Регистрируем джедая
service.registerJedi('Оби-Ван', 'Мастер');
// Проверяем, что можем получить его данные
const info = service.getJediInfo('Оби-Ван');
expect(info).toBe('Оби-Ван - Мастер');
});
test('выбрасывает ошибку для несуществующего джедая', () => {
expect(() => {
service.getJediInfo('Дарт Вейдер');
}).toThrow('Джедай не найден');
});
});
🎭 3. E2E Тесты (End-to-End)
Тестируют полный пользовательский сценарий от начала до конца.
Playwright / JavaScript
const { test, expect } = require('@playwright/test');
test('пользователь может зарегистрировать джедая', async ({ page }) => {
// Открываем страницу
await page.goto('http://jedi.codes/register');
// Заполняем форму
await page.fill('input[name="name"]', 'Люк Скайуокер');
await page.fill('input[name="rank"]', 'Рыцарь');
await page.selectOption('select[name="specialization"]', 'guardian');
// Отправляем форму
await page.click('button[type="submit"]');
// Проверяем успешную регистрацию
await expect(page.locator('.success-message')).toContainText(
'Джедай успешно зарегистрирован'
);
// Проверяем, что джедай появился в списке
await page.goto('http://jedi.codes/jedis');
await expect(page.locator('.jedi-list')).toContainText('Люк Скайуокер');
});
🎯 4. TDD — Test-Driven Development
Методология разработки, при которой тесты пишутся до кода.
🔴 Red
Пиши тест, который не проходит
🟢 Green
Напиши минимум кода, чтобы тест прошёл
🔵 Refactor
Улучши код, сохраняя прохождение тестов
Пример TDD цикла
// Шаг 1: RED - Пишем тест (он упадёт)
test('calculateForceLevel возвращает уровень силы', () => {
expect(calculateForceLevel(100, 0.5)).toBe(50);
});
// Шаг 2: GREEN - Простейшая реализация
function calculateForceLevel(midichlorians, training) {
return midichlorians * training;
}
// Шаг 3: REFACTOR - Улучшаем код
function calculateForceLevel(midichlorians, training) {
if (midichlorians < 0 || training < 0) {
throw new Error('Значения не могут быть отрицательными');
}
if (training > 1) {
throw new Error('Уровень тренировки должен быть от 0 до 1');
}
return Math.round(midichlorians * training);
}
// Добавляем больше тестов
test('выбрасывает ошибку при отрицательных значениях', () => {
expect(() => calculateForceLevel(-100, 0.5)).toThrow();
});
test('округляет результат', () => {
expect(calculateForceLevel(101, 0.5)).toBe(51);
});
🎪 5. Моки и Стабы
Инструменты для изоляции тестируемого кода от зависимостей.
Jest Mocks
// Сервис, который нужно протестировать
class JediNotificationService {
constructor(emailService, smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
notifyJedi(jedi, message) {
const results = [];
if (jedi.email) {
results.push(this.emailService.send(jedi.email, message));
}
if (jedi.phone) {
results.push(this.smsService.send(jedi.phone, message));
}
return results.every(r => r === true);
}
}
// Тест с моками
describe('JediNotificationService', () => {
test('отправляет уведомления по всем каналам', () => {
// Создаём моки
const mockEmailService = {
send: jest.fn().mockReturnValue(true)
};
const mockSmsService = {
send: jest.fn().mockReturnValue(true)
};
const service = new JediNotificationService(
mockEmailService,
mockSmsService
);
const jedi = {
name: 'Йода',
email: 'yoda@jedi.com',
phone: '+1234567890'
};
// Вызываем метод
const result = service.notifyJedi(jedi, 'Срочное собрание!');
// Проверяем результат
expect(result).toBe(true);
// Проверяем, что методы были вызваны
expect(mockEmailService.send).toHaveBeenCalledWith(
'yoda@jedi.com',
'Срочное собрание!'
);
expect(mockSmsService.send).toHaveBeenCalledWith(
'+1234567890',
'Срочное собрание!'
);
});
});
📊 6. Покрытие Кода (Code Coverage)
Метрика, показывающая процент кода, покрытого тестами.
Строки (Lines)
Процент выполненных строк кода
Ветви (Branches)
Процент протестированных веток условий
Функции (Functions)
Процент вызванных функций
Выражения (Statements)
Процент выполненных выражений
Запуск с покрытием
# Jest с покрытием
npm test -- --coverage
# Вывод покрытия в консоль
------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
------------|---------|----------|---------|---------|
All files | 87.5 | 75.0 | 100.0 | 87.5 |
jedi.js | 87.5 | 75.0 | 100.0 | 87.5 |
------------|---------|----------|---------|---------|
💎 Золотые Правила Тестирования
- FIRST Principle: Fast (быстрые), Independent (независимые), Repeatable (повторяемые), Self-validating (самопроверяющиеся), Timely (своевременные)
- AAA Pattern: Arrange (подготовка), Act (действие), Assert (проверка)
- Один тест — одна проверка
- Тесты должны быть понятны без чтения кода
- Не тестируй приватные методы напрямую
- 100% покрытие — не гарантия качества
- Тесты — это документация кода
- Красный-Зелёный-Рефакторинг (TDD цикл)
- Моки используй разумно — не злоупотребляй
- Тесты должны быть детерминированными
🎯 Что Тестировать В Первую Очередь
🔴 Высокий приоритет
- Критическая бизнес-логика
- Обработка платежей
- Аутентификация и авторизация
- Сложные вычисления
🟡 Средний приоритет
- Утилиты и хелперы
- Валидация данных
- API endpoints
- Преобразование данных
🟢 Низкий приоритет
- Геттеры и сеттеры
- Простые UI компоненты
- Конфигурационные файлы
- Константы