Lesson 18 of 26
Урок 18 — Testing Layers (Пирамида тестирования)
Название: Testing Layers — уровни тестирования и пирамида тестов
Описание: Разбираем уровни тестирования: Unit, Integration, E2E. Изучаем пирамиду тестирования, понимаем, почему важно правильное соотношение тестов и как это влияет на скорость и надёжность CI/CD.
Почему это важно для QA: Понимание уровней тестирования — основа для грамотного планирования тест-стратегии. Без этого команды создают «мороженое-антипаттерн» — много E2E, мало unit — и страдают от медленных и нестабильных пайплайнов.
1. Пирамида тестирования
Пирамида тестирования — это концепция, предложенная Майком Коном (Mike Cohn), которая описывает идеальное соотношение разных уровней тестов.
/\
/E2E\ ← мало тестов, медленные, дорогие
/──────\
/ Integ- \ ← умеренное количество
/ ration \
/──────────────\
/ Unit Tests \ ← много тестов, быстрые, дешёвые
/──────────────────\Принципы пирамиды
| Уровень | Количество | Скорость | Стоимость поддержки |
|---|---|---|---|
| Unit | Много (70%) | Секунды | Низкая |
| Integration | Средне (20%) | Минуты | Средняя |
| E2E | Мало (10%) | Десятки минут | Высокая |
2. Unit Testing — Первый уровень
Unit Testing — тестирование отдельных функций, методов, классов в изоляции от остальной системы.
Характеристики
- Тестирует минимальный модуль (функцию, метод)
- Выполняется очень быстро (миллисекунды)
- Не требует базы данных, браузера, сети
- Пишется разработчиком
- Обнаруживает ошибки на раннем этапе
Пример Unit Test (JavaScript / Jest)
// Функция, которую тестируем
function calculateTax(price, taxRate) {
if (price < 0) throw new Error("Цена не может быть отрицательной");
if (taxRate < 0 || taxRate > 1) throw new Error("Ставка налога должна быть от 0 до 1");
return price * taxRate;
}
// Unit тесты
describe("calculateTax", () => {
test("должен вернуть правильный налог", () => {
expect(calculateTax(1000, 0.2)).toBe(200);
});
test("должен выбросить ошибку при отрицательной цене", () => {
expect(() => calculateTax(-100, 0.2)).toThrow("Цена не может быть отрицательной");
});
test("должен выбросить ошибку при неверной ставке", () => {
expect(() => calculateTax(1000, 1.5)).toThrow("Ставка налога");
});
test("должен вернуть 0 при нулевом налоге", () => {
expect(calculateTax(1000, 0)).toBe(0);
});
});
Что такое Mock / Stub / Spy
В unit тестах зависимости заменяются на имитации:
// Mock: полная замена зависимости
const emailServiceMock = {
sendEmail: jest.fn().mockResolvedValue({ success: true }),
};
// Stub: заготовленный ответ
const userRepositoryStub = {
findById: jest.fn().mockReturnValue({ id: 1, name: "John" }),
};
// Spy: наблюдение за реальной функцией
const consoleSpy = jest.spyOn(console, "log");
3. Integration Testing — Второй уровень
Integration Testing — тестирование взаимодействия между несколькими модулями или компонентами системы.
Характеристики
- Тестирует несколько компонентов вместе
- Может использовать реальную (или тестовую) БД
- Медленнее unit тестов
- Находит ошибки в интерфейсах между модулями
Что тестируем
Примеры интеграционных тестов:
├── UserService + UserRepository (сервис + БД)
├── API-эндпоинт + бизнес-логика + БД
├── Frontend компонент + API-запрос
└── Message Queue + Consumer (очереди сообщений)Пример Integration Test (API + БД)
import request from "supertest";
import { app } from "../app";
import { database } from "../database";
describe("POST /api/users — интеграция с БД", () => {
beforeEach(async () => {
await database.truncate("users");
});
afterAll(async () => {
await database.close();
});
test("должен создать пользователя в БД", async () => {
const newUser = {
name: "Иван Петров",
email: "ivan@test.com",
password: "SecurePass123",
};
const response = await request(app)
.post("/api/users")
.send(newUser)
.expect(201);
// Проверяем ответ API
expect(response.body.id).toBeDefined();
expect(response.body.email).toBe(newUser.email);
// Проверяем реальную запись в БД
const userInDb = await database.findOne("users", { email: newUser.email });
expect(userInDb).not.toBeNull();
expect(userInDb.name).toBe(newUser.name);
});
});
4. End-to-End Testing (E2E) — Третий уровень
E2E Testing — тестирование полного пользовательского сценария от начала до конца, через весь стек приложения.
Характеристики
- Тестирует реальное приложение в браузере
- Использует реальную БД, API, frontend
- Медленное (минуты на тест)
- Дорогое в поддержке
- Находит системные проблемы и проблемы интеграции UI
Пример E2E теста в Playwright
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";
import { CartPage } from "../pages/CartPage";
import { CheckoutPage } from "../pages/CheckoutPage";
// E2E: полный путь покупки товара
test("пользователь может купить товар от входа до подтверждения", async ({
page,
}) => {
const loginPage = new LoginPage(page);
const cartPage = new CartPage(page);
const checkoutPage = new CheckoutPage(page);
await test.step("авторизоваться", async () => {
await loginPage.goto();
await loginPage.login("user@test.com", "TestPass123");
await expect(page).toHaveURL("/dashboard");
});
await test.step("найти товар и добавить в корзину", async () => {
await page.goto("/catalog");
await page.getByText("iPhone 15 Pro").click();
await page.getByRole("button", { name: "В корзину" }).click();
await expect(cartPage.cartBadge).toContainText("1");
});
await test.step("оформить заказ", async () => {
await cartPage.goto();
await cartPage.proceedToCheckout();
await checkoutPage.fillDeliveryAddress({
street: "ул. Тестовая, 1",
city: "Москва",
zip: "123456",
});
await checkoutPage.selectPaymentMethod("card");
await checkoutPage.confirmOrder();
});
await test.step("убедиться в успешном заказе", async () => {
await expect(page).toHaveURL(/\/orders\/\d+/);
await expect(page.getByText("Заказ успешно оформлен")).toBeVisible();
});
});
5. Антипаттерны пирамиды
Антипаттерн 1: «Мороженое» (Ice Cream Cone)
/──────────────────\
/ Много E2E \ ← 70% тестов
/──────────────────────\
/ Немного Integration \ ← 20% тестов
/────────────────────────────\
/ Мало Unit \ ← 10% тестовПроблемы:
- CI/CD пайплайн занимает часы
- Тесты постоянно мигают (flaky)
- Сложно найти причину провала
Антипаттерн 2: «Песочные часы» (Hourglass)
/────────────────────\
/ Много Unit \
/────────────────────────\
|
| ← нет integration тестов
|
/────────────────────────\
/ Много E2E \Проблемы: ошибки интеграции не находятся ни unit, ни E2E тестами.
6. Уровни тестирования в контексте QA и разработки
Кто пишет каждый уровень:
Unit Tests ← Разработчики (Dev)
Integration ← Разработчики + QA Automation (SDET)
E2E Tests ← QA Automation Engineers
Manual Testing ← QA Engineers (ручное тестирование)7. Соотношение тестов в реальном проекте
Проект: интернет-магазин
Unit Tests (70% — ~500 тестов):
├── Функции расчёта скидок
├── Валидация входных данных
├── Бизнес-логика заказов
└── Утилитарные функции
Integration Tests (20% — ~150 тестов):
├── API-эндпоинты с тестовой БД
├── Взаимодействие сервисов
└── Внешние интеграции (email, payment)
E2E Tests (10% — ~50 тестов):
├── Критические пользовательские пути (happy path)
├── Авторизация
├── Покупка товара
└── Управление аккаунтом
Время выполнения:
├── Unit: ~30 секунд
├── Integration: ~5 минут
└── E2E: ~20 минут8. Почему E2E тесты нестабильны
E2E тесты работают с реальным браузером и сетью, поэтому они чаще мигают:
Причины нестабильности E2E:
├── Сетевые задержки
├── Анимации и переходы
├── Асинхронные операции
├── Зависимость от тестовых данных
├── Конкурентные тесты
└── Нестабильное тестовое окружениеКак бороться:
- Правильные ожидания вместо
waitForTimeout - Изолированные тестовые данные
- Запуск в stale state (чистое состояние перед каждым тестом)
- Retry стратегия для нестабильных тестов
Итог
TESTING LAYERS (ПИРАМИДА)
├── Unit (70%): быстро, изолированно, много тестов
├── Integration (20%): взаимодействие модулей
├── E2E (10%): полный сценарий в браузере, мало тестов
│
├── Антипаттерн: мороженое (много E2E, мало unit) → медленный CI
├── Идеал: широкое основание из unit + интеграция + немного E2E
│
└── Правило: чем выше в пирамиде — тем медленнее и дороже в поддержке