Жизненный цикл тестирования и виды тестов
Жизненный цикл тестирования и виды тестов
Представьте, что вы строите мост. Вы не будете ждать окончания строительства, чтобы пустить по нему колонну тяжелых грузовиков и посмотреть, выдержит ли конструкция. Инженеры проверяют качество бетона еще на заводе, затем тестируют отдельные опоры, оценивают прочность соединений и только потом проводят финальные испытания всей конструкции.
В разработке программного обеспечения действует тот же принцип. Тестирование программного обеспечения — это систематический процесс проверки приложения на соответствие заданным требованиям. Его главная цель — найти ошибки (баги) до того, как продукт попадет к пользователю.
Зачем это нужно знать разработчику? Ошибка, найденная на этапе написания кода, стоит копейки: программист просто стирает строку и пишет новую. Ошибка, найденная после релиза, может стоить компании миллионов. Например, если в интернет-магазине сломается кнопка «Оплатить», при потоке в 1000 клиентов в час со средним чеком 5000 руб., компания будет терять 5 000 000 руб. каждый час простоя.
Жизненный цикл тестирования (STLC)
Многие новички думают, что тестирование — это хаотичное «прокликивание» готовой программы. На самом деле это строгий инженерный процесс, который идет параллельно с написанием кода. Этот процесс называется жизненным циклом тестирования (Software Testing Life Cycle, STLC).
STLC состоит из шести последовательных этапов. Пропуск любого из них неизбежно ведет к снижению качества продукта.
1. Анализ требований
На этом этапе команда изучает, что именно должна делать программа. Тестировщики и разработчики читают документацию и задают вопросы бизнесу. Главная задача здесь — убедиться, что требования логичны, полны и не противоречат друг другу.
Требование: «Пароль пользователя должен быть безопасным». Это плохое требование, потому что понятие «безопасный» субъективно. Хорошее требование: «Пароль должен состоять минимум из 8 символов, содержать хотя бы одну заглавную букву и одну цифру».
2. Планирование тестирования
Команда определяет стратегию: что мы будем тестировать, какие инструменты использовать (например, библиотеку pytest для Python), сколько времени это займет и кто будет за это отвечать. Результатом этого этапа становится документ — тест-план.
3. Тест-дизайн
Это этап проектирования проверок. Специалисты создают тест-кейсы — пошаговые инструкции, описывающие, что нужно сделать, какие данные ввести и какой результат ожидается.
Например, для функции деления двух чисел и тест-кейсы будут такими:
- Позитивный сценарий: , . Ожидаемый результат: .
- Негативный сценарий: , . Ожидаемый результат: ошибка деления на ноль.
4. Настройка тестовой среды
Программу нельзя тестировать там же, где ее пишут разработчики, или там, где ею пользуются реальные клиенты. Для проверок создается отдельная изолированная среда — сервер с нужной версией операционной системы, базой данных и тестовыми (ненастоящими) пользователями.
5. Выполнение тестов
Только на пятом этапе начинается фактическая проверка кода. Тесты запускаются вручную или автоматически. Если фактический результат работы программы не совпадает с ожидаемым (из тест-кейса), заводится баг-репорт — отчет об ошибке, который отправляется разработчику на исправление.
6. Завершение тестирования
Когда все тесты пройдены, а критические ошибки исправлены, команда собирает метрики. Одной из важных метрик является покрытие кода тестами. Оно рассчитывается по формуле:
Где:
- — процент покрытия кода тестами
- — количество строк кода, которые были вызваны во время выполнения тестов
- — общее количество строк кода в проекте
Если из 1000 строк кода тесты проверили 850, покрытие составит 85%.
Пирамида тестирования и виды тестов
Программы состоят из множества слоев: от крошечных функций до сложных пользовательских интерфейсов. Проверять все это одним способом неэффективно. В индустрии принята концепция пирамиды тестирования, которая делит тесты на три основных уровня.
Юнит-тестирование (Модульное тестирование)
Это фундамент пирамиды. Юнит-тесты проверяют самые мелкие, изолированные части кода — отдельные функции или методы классов. Они пишутся самими разработчиками и выполняются за миллисекунды.
Зачем это нужно: юнит-тесты локализуют ошибку. Если тест упал, вы точно знаете, в какой именно функции проблема, и вам не нужно искать баг по всему проекту.
Рассмотрим пример на Python. У нас есть простая функция расчета итоговой стоимости товара с учетом скидки:
def calculate_total(price, discount_percent):
if discount_percent < 0 or discount_percent > 100:
raise ValueError("Скидка должна быть от 0 до 100")
discount_amount = price * (discount_percent / 100)
return price - discount_amount
Юнит-тест для этой функции будет выглядеть так:
def test_calculate_total_normal_discount():
# Подготовка данных
price = 1000
discount = 20
# Вызов функции
result = calculate_total(price, discount)
# Проверка результата (утверждение)
assert result == 800.0
В этом тесте мы проверяем только математику внутри одной конкретной функции. Мы не подключаемся к базе данных и не открываем браузер.
Интеграционное тестирование
Когда отдельные функции (юниты) проверены и работают правильно, их нужно объединить. Интеграционные тесты проверяют, как различные модули системы взаимодействуют друг с другом или с внешними сервисами (базами данных, сторонними API).
Зачем это нужно: две функции могут идеально работать по отдельности, но при попытке передать данные из одной в другую произойдет сбой из-за несовпадения форматов.
Пример из жизни: вы купили отличный японский телевизор (юнит 1) и отличную европейскую игровую приставку (юнит 2). По отдельности они исправны. Но когда вы пытаетесь подключить приставку к телевизору, выясняется, что у них разные стандарты вилок для розетки. Это ошибка интеграции.
В коде интеграционный тест может проверять, правильно ли функция сохранения пользователя записывает данные в реальную базу данных PostgreSQL.
Функциональное тестирование (End-to-End)
На вершине пирамиды находятся тесты, которые проверяют систему целиком, имитируя действия реального пользователя. Это сквозное тестирование (End-to-End, E2E).
Зачем это нужно: бизнесу не так важно, как работают отдельные функции в коде. Бизнесу важно, чтобы клиент мог зайти на сайт, положить товар в корзину и оплатить его.
При функциональном тестировании веб-приложения специальная программа (например, Selenium или Playwright) открывает настоящий браузер, кликает по кнопкам, заполняет формы и проверяет, появилось ли на экране сообщение об успешной оплате. Такие тесты пишутся долго, выполняются медленно и требуют сложной настройки среды, поэтому их должно быть меньше всего в проекте.
| Характеристика | Юнит-тесты | Интеграционные тесты | Функциональные (E2E) |
|---|---|---|---|
| Что проверяют | Отдельные функции | Связи между модулями | Весь путь пользователя |
| Скорость выполнения | Миллисекунды | Секунды | Минуты |
| Кто пишет | Разработчики | Разработчики / Тестировщики | Тестировщики (QA Automation) |
| Количество в проекте | Тысячи | Сотни | Десятки |
Разработка через тестирование (TDD)
Традиционный подход к программированию выглядит так: разработчик пишет код, затем запускает его, проверяет глазами, и если все работает, пишет тесты (или не пишет их вообще, ссылаясь на нехватку времени).
Разработка через тестирование (Test-Driven Development, TDD) переворачивает этот процесс с ног на голову. В TDD вы сначала пишете тест для функции, которой еще не существует, и только потом пишете сам код.
Этот подход опирается на строгий цикл из трех шагов, который часто называют Red-Green-Refactor (Красный-Зеленый-Рефакторинг).
Шаг 1: Red (Красный)
Вы пишете тест, который описывает желаемое поведение новой функции. Поскольку самой функции еще нет, при запуске тест неизбежно падает (в консоли результат подсвечивается красным цветом).
Например, мы хотим создать функцию, которая проверяет, является ли строка палиндромом (читается одинаково слева направо и справа налево). Мы пишем тест:
def test_is_palindrome():
assert is_palindrome("radar") == True
assert is_palindrome("python") == False
Запускаем — получаем ошибку NameError: name 'is_palindrome' is not defined. Это отлично, мы зафиксировали наши требования в виде кода.
Шаг 2: Green (Зеленый)
Теперь ваша задача — написать минимально возможный код, чтобы тест прошел (загорелся зеленым). На этом этапе не нужно думать о красоте кода или архитектуре. Главное — удовлетворить тест.
def is_palindrome(word):
if word == "radar":
return True
return False
Да, код выглядит глупо, но тест теперь проходит! Мы доказали, что система может перейти из нерабочего состояния в рабочее.
Шаг 3: Refactor (Рефакторинг)
Теперь, когда у вас есть работающий код, защищенный тестом, вы можете его улучшить (отрефакторить). Вы меняете внутреннюю реализацию функции, делая ее универсальной и элегантной. Если вы где-то ошибетесь, тест сразу загорится красным и предупредит вас.
def is_palindrome(word):
# Универсальное решение: сравниваем слово с его перевернутой версией
return word == word[::-1]
Запускаем тест — он все еще зеленый. Цикл завершен.
Зачем применять TDD?
- 100% покрытие тестами: код пишется только для того, чтобы пройти тест. Непротестированного кода просто не существует.
- Защита от переусложнения: вы пишете ровно столько кода, сколько нужно для решения конкретной задачи, избегая создания ненужных функций «на будущее».
- Уверенность при изменениях: разработчик не боится менять архитектуру, потому что тесты моментально покажут, если что-то сломается.
Понимание жизненного цикла тестирования, умение выбирать правильный уровень пирамиды для проверки своего кода и применение практик вроде TDD — это то, что отличает начинающего программиста от уверенного инженера. Эти концепции станут фундаментом, на котором мы будем строить изучение конкретных инструментов Python в следующих материалах.
