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)

javascriptjavascript
// Функция, которую тестируем
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 тестах зависимости заменяются на имитации:

javascriptjavascript
// 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 + БД)

typescripttypescript
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

typescripttypescript
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
│
└── Правило: чем выше в пирамиде — тем медленнее и дороже в поддержке