Основы тестирования: пирамида, жизненный цикл и виды тестов
Основы тестирования: пирамида, жизненный цикл и виды тестов
Добро пожаловать на курс «Автоматизированное тестирование на Python: Путь от Junior до Senior». Мы начинаем с фундамента. Прежде чем писать код на pytest или разворачивать Docker-контейнеры, необходимо разобраться в архитектуре качества. Без понимания того, что и зачем мы тестируем, даже самый сложный код будет бесполезен.
В этом модуле мы разберем теорию, которая отличает инженера по автоматизации от простого «писателя скриптов», и напишем первые тесты на чистом Python.
Цель модуля
К концу этого занятия вы будете:
- Понимать концепцию Пирамиды тестирования и избегать антипаттернов.
- Знать этапы Жизненного цикла тестирования (STLC).
- Различать виды тестов: Unit, Integration, E2E, Smoke, Regression.
- Уметь применять паттерн AAA (Arrange-Act-Assert) на практике.
Теория
1. Пирамида тестирования (Testing Pyramid)
Это ключевая ментальная модель для QA-инженера. Она определяет стратегию автоматизации: сколько и каких тестов нужно писать.
Уровни пирамиды (снизу вверх):
-
Unit Tests (Модульные тесты)
- Суть: Проверка наименьшей единицы кода (функции, метода) в полной изоляции. Никаких баз данных, сети или файловой системы.
- Скорость: Мгновенная (миллисекунды).
- Количество: 70-80% от всех тестов.
- Инструменты:
unittest,pytest.
-
Integration Tests (Интеграционные тесты)
- Суть: Проверка взаимодействия между компонентами (например, сервис + база данных, или сервис A + сервис B).
- Скорость: Средняя (секунды).
- Количество: 15-20%.
- Инструменты:
pytest,requests, Docker.
-
E2E / UI Tests (Сквозные тесты)
- Суть: Эмуляция действий реального пользователя через интерфейс. Проверяем систему целиком.
- Скорость: Медленная (минуты).
- Количество: 5-10% (только критические сценарии).
- Инструменты: Selenium, Playwright.
Антипаттерн «Рожок мороженого» (Ice Cream Cone): Ситуация, когда проект перегружен медленными UI-тестами при отсутствии Unit-тестов. Это ведет к нестабильности (flaky tests) и долгому ожиданию результатов.
2. Жизненный цикл тестирования (STLC)
Автоматизация не существует в вакууме. Она встраивается в процесс STLC (Software Testing Life Cycle):
- Requirement Analysis: Понимаем, что тестировать. Если требование нелогично, писать автотест рано.
- Test Planning: Выбираем инструменты (Python + pytest) и стратегию.
- Test Case Design: Пишем сценарии. Важно: хороший автотест рождается из хорошего ручного тест-кейса.
- Environment Setup: Настройка тестовой среды (БД, тестовые данные).
- Execution: Запуск тестов (CI/CD).
- Closure: Анализ отчетов и покрытия кода.
3. Виды тестов по цели запуска
- Smoke Testing (Дымовое): Быстрая проверка критического функционала («Запускается ли приложение?», «Работает ли логин?»). Если дым идет — глубокое тестирование не имеет смысла.
- Regression Testing (Регрессионное): Полный прогон тестов после внесения изменений в код. Гарантия того, что новые фичи не сломали старые. Это главная задача автотестов.
4. Паттерн AAA (Arrange-Act-Assert)
Золотой стандарт структуры теста. Если ваши тесты не следуют этому паттерну, их будет сложно читать и поддерживать.
- Arrange (Подготовка): Создаем объекты, настраиваем моки, готовим данные.
- Act (Действие): Вызываем тестируемый метод.
- Assert (Проверка): Сравниваем фактический результат с ожидаемым.
Полезные ссылки
- The Practical Test Pyramid (Martin Fowler) — библия для автоматизатора.
- Software Testing Life Cycle — подробнее про этапы.
Практика
В этом модуле мы используем чистый Python и оператор assert, чтобы понять механику проверок без «магии» фреймворков.
Задача 1: Чистая функция (Unit-тест)
Дано: Функция расчета скидки. Задача: Написать тест, проверяющий корректность расчета и валидацию входных данных.
# app.py
def calculate_discount(price, discount_percent):
if price < 0 or discount_percent < 0:
raise ValueError("Values must be positive")
if discount_percent > 100:
raise ValueError("Discount cannot exceed 100%")
return price * (1 - discount_percent / 100)
# test_app.py
def test_calculate_discount():
# 1. Arrange
price = 1000
percent = 20
expected = 800
# 2. Act
result = calculate_discount(price, percent)
# 3. Assert
assert result == expected, f"Expected {expected}, but got {result}"
print("Unit-тест пройден!")
test_calculate_discount()
Задача 2: Тестирование состояния (Stateful Testing)
Дано: Класс User, который может менять статус.
Задача: Проверить переход состояния.
class User:
def __init__(self, name, is_active=True):
self.name = name
self.is_active = is_active
def ban(self):
self.is_active = False
def test_user_ban():
# Arrange
user = User("Alex")
# Pre-assertion (проверяем исходное состояние)
assert user.is_active is True, "User should be active initially"
# Act
user.ban()
# Assert
assert user.is_active is False, "User should be banned after ban() call"
print("Тест состояния пройден!")
test_user_ban()
Задача 3: Ручной Mock (Интеграционный тест)
Дано: Сервис отправки уведомлений, зависящий от внешнего провайдера (Email). Задача: Протестировать логику сервиса, подменив реальную отправку писем «заглушкой» (Mock), чтобы не спамить реальными письмами.
# --- Реальный код ---
class EmailProvider:
def send(self, message):
# Представим, что тут сложная логика отправки по сети
print(f"Sending email: {message}")
class NotificationService:
def __init__(self, provider):
self.provider = provider
def notify_admin(self, error_msg):
formatted_msg = f"CRITICAL ERROR: {error_msg}"
self.provider.send(formatted_msg)
# --- Тестовый код ---
# Создаем Mock-объект (имитацию)
class MockEmailProvider:
def __init__(self):
self.sent_messages = []
def send(self, message):
self.sent_messages.append(message)
def test_notification_service():
# Arrange
mock_provider = MockEmailProvider() # Внедряем зависимость
service = NotificationService(mock_provider)
# Act
service.notify_admin("Database down")
# Assert
# 1. Проверяем, что метод send был вызван ровно 1 раз
assert len(mock_provider.sent_messages) == 1, "Email should be sent once"
# 2. Проверяем, что сообщение отформатировано верно
expected_msg = "CRITICAL ERROR: Database down"
actual_msg = mock_provider.sent_messages[0]
assert actual_msg == expected_msg, f"Wrong message format. Got: {actual_msg}"
print("Интеграционный тест с моком пройден!")
test_notification_service()
Домашнее задание
Контекст: Вы разрабатываете систему для интернет-магазина. Есть класс Cart (Корзина), который позволяет добавлять товары (словарь {'name': str, 'price': int}), удалять их и считать общую сумму.
Задание:
- Реализуйте класс
Cartс методамиadd_item(item),remove_item(item_name),calculate_total(). - Напишите 3 теста (функции), используя
assertи паттерн AAA:- Тест добавления товара (проверить, что товар появился в списке и сумма изменилась).
- Тест удаления товара.
- Тест пустой корзины (сумма должна быть 0).
- Критерии приемки:
- Код запускается без ошибок.
- При изменении логики
calculate_total(например, добавить +1 к сумме) тесты должны падать с ошибкойAssertionError. - В каждом тесте есть комментарии
# Arrange,# Act,# Assert.
Типичные ошибки новичков
- «Немые» ассерты:
- Плохо:
assert result == 5(Если упадет, вы увидите простоAssertionError). - Хорошо:
assert result == 5, f"Expected 5, but got {result}"(Сразу понятно, что пошло не так).
- Плохо:
- Зависимые тесты: Один тест меняет глобальную переменную, а второй падает из-за этого. Каждый тест должен быть изолирован (создавать свои данные заново).
- Тестирование всего сразу: Один тест проверяет и логин, и корзину, и оплату. Если он упадет, непонятно, где ошибка. Следуйте правилу: Один тест — одна проверка.
Чек-лист прогресса
- [ ] Я понимаю разницу между Unit и E2E тестами.
- [ ] Я знаю, что такое AAA и использую его в коде.
- [ ] Я написал тест с использованием
assertи кастомным сообщением об ошибке. - [ ] Я понимаю, почему нельзя писать только UI-тесты (пирамида).