Подготовка среды и создание первого теста
Проверка в БД — SQL для тестировщика, Основы БД, транзакции, PostgreSQL. Карта — о разделе.
Практикум. Вы уже знаете, что такое тест (из основ и документации). Здесь — первый автотест своими руками — Python, pytest, принцип изоляции и моки. Не обязательно становиться разработчиком, но понимание автотеста помогает говорить с SDET на одном языке.
Play ITЗагрузка интерактивного демо…
Юнит-тестирование и принцип изоляции
Юнит-тест представляет собой автоматизированную проверку отдельной единицы программного кода. В контексте разработки программных продуктов такой единицей обычно выступает функция, метод класса или небольшой модуль логики. Основная цель юнит-теста заключается в подтверждении того, что конкретный участок кода выполняет свои задачи корректно при заданных входных данных.
Процесс создания юнит-тестов неразрывно связан с циклом разработки. Разработчик пишет код функции, затем сразу создает тест для проверки его работы. Тест фиксирует ожидаемое поведение системы. Если изменения в коде нарушают логику работы функции, тест сигнализирует об ошибке. Такой подход позволяет обнаруживать регрессии на ранних стадиях. Регрессия означает появление новых ошибок в ранее работавшем функционале после внесения изменений.
Ключевым принципом юнит-тестирования является изоляция. Изоляция требует проверки функции в отрыве от внешних зависимостей. Внешние зависимости включают базы данных, сетевые запросы к API, файловую систему, время и случайные генераторы чисел. При наличии таких зависимостей тест перестает быть быстрым и надежным. Скорость выполнения падает из-за ожидания ответа от сети или диска. Надежность снижается из-за нестабильности внешней среды. Например, база данных может временно недоступна, а сервер может вернуть ошибку.
Изолированный тест гарантирует, что проверка касается только логики конкретной функции. Результат теста зависит исключительно от переданных аргументов и внутренней реализации проверяемого кода. Это упрощает поиск причин сбоя. Если тест падает, разработчик знает, что проблема находится внутри тестируемой функции, а не в работе базы данных или сети.
Настройка окружения и структура проекта
Для проведения практики используется язык программирования Python и фреймворк pytest.
Pytest обеспечивает удобный синтаксис написания тестов и автоматическое их обнаружение. Установка инструмента происходит через менеджер пакетов pip. Команда установки выглядит следующим образом:
pip install pytest
Создание структуры проекта начинается с формирования директории для приложения и поддиректории для тестов. Рекомендуется разделять исходный код и тестовый код в разные папки. Это делает проект чистым и понятным.
Структура файлов выглядит так:
project_root/
├── app/
│ └── calculator.py
├── tests/
│ ├── __init__.py
│ └── test_calculator.py
└── requirements.txt
Файл calculator.py содержит логику приложения. Файл test_calculator.py содержит набор тестов для этой логики. Файл requirements.txt хранит список зависимостей проекта.
tests/__init__.pyВ старых проектах в tests/ кладут пустой __init__.py, чтобы Python видел пакет. Для pytest в современных версиях файл часто не обязателен. Если импорт from app.calculator import add падает с ModuleNotFoundError, запускайте тесты из корня проекта (pytest) или установите пакет в editable-режиме: pip install -e . — так делает большинство команд.
pip install -e .
Переход в корневую директорию проекта и запуск команды pytest автоматически находит все файлы, начинающиеся с test_ или заканчивающиеся _test.py. Pytest сканирует содержимое этих файлов в поисках функций, имена которых начинаются с test_. Найденные функции выполняются как тестовые кейсы.
Первая функция и базовый тест
Разработка начинается с создания простой функции сложения двух чисел. Этот пример демонстрирует базовую логику и возможность ее проверки. Функция принимает два аргумента и возвращает их сумму.
Содержимое файла app/calculator.py:
def add(a, b):
return a + b
Функция add имеет два параметра a и b. Оператор return передает результат вычисления вызывающему коду. Код функции минималистичен, но он полностью соответствует требованиям для юнит-тестирования.
Создание теста для этой функции выполняется в файле tests/test_calculator.py. Тест должен импортировать функцию из основного модуля и вызывать её с известными значениями. Результат вызова сравнивается с ожидаемым значением.
Содержимое файла tests/test_calculator.py:
from app.calculator import add
def test_add_positive_numbers():
result = add(2, 3)
assert result == 5
Тестовая функция test_add_positive_numbers импортирует функцию add из модуля app.calculator. Вызов add(2, 3) возвращает значение 5. Утверждение assert проверяет равенство полученного результата и ожидаемого значения 5. Если условие истинно, тест проходит успешно. Если условие ложно, pytest сообщает об ошибке и показывает разницу между фактическим и ожидаемым результатом.
Запуск из корня проекта:
pytest
При успешном выполнении вывод терминала показывает зеленую галочку и статус passed. Пример вывода:
tests/test_calculator.py . [100%]
1 passed in 0.01s
Значок точки указывает на пройденный тест. Статус 1 passed подтверждает выполнение одного теста без ошибок. Время выполнения 0.01s демонстрирует высокую скорость работы юнит-теста.
Демонстрация падения теста
Изменение поведения функции позволяет увидеть работу механизма тестирования в действии. Искусственное изменение кода функции приводит к тому, что она начинает возвращать неверное значение. Это моделирует ситуацию ошибки в разработке или регрессию.
Измененная версия функции add в файле app/calculator.py:
def add(a, b):
return a - b
В этом случае функция выполняет вычитание вместо сложения. Логика программы изменилась, но тестовый код остался прежним. Запуск теста снова вызывает механизм проверки.
Результат запуска команды pytest:
tests/test_calculator.py F [100%]
=================================== FAILURES ===================================
___________________________ test_add_positive_numbers ___________________________
def test_add_positive_numbers():
result = add(2, 3)
> assert result == 5
E assert -1 == 5
tests/test_calculator.py:6: AssertionError
Знак F в выводе обозначает провал теста (failed). Сообщение AssertionError указывает на нарушение условия утверждения. Строка E assert -1 == 5 показывает фактическое значение -1 и ожидаемое значение 5. Разница между ними очевидна. Тест выявил несоответствие между поведением функции и заявленными требованиями.
Это поведение критически важно для процесса разработки. Автоматическая проверка мгновенно сигнализирует о проблеме. Разработчик получает точную информацию о том, где произошло отклонение. Исправление ошибки возвращается к исходному коду функции:
def add(a, b):
return a + b
Повторный запуск теста возвращает статус passed. Система гарантирует, что исправление вернуло функциональность к правильному состоянию.
Работа с моками и внешними зависимостями
Реальные функции часто зависят от внешних ресурсов. База данных, внешний API или файловая система создают проблемы при тестировании. Эти ресурсы могут работать медленно, быть недоступными или возвращать случайные данные. Использование реальных ресурсов делает тесты медленными и ненадежными.
Мок (mock) — это объект, который имитирует поведение реальной зависимости. Мок предоставляет предопределенные ответы на вызовы методов. Это позволяет изолировать тестируемую функцию от внешнего мира. Тест работает только с логикой функции, а не с состоянием базы данных или сетью.
Библиотека unittest.mock входит в стандартную поставку Python. Она предоставляет инструменты для создания мок-объектов. Пример использования моков демонстрирует ситуацию, когда функция запрашивает данные из базы данных.
Предположим, существует функция get_user_data, которая обращается к базе данных для получения информации о пользователе.
Содержимое файла app/user_service.py:
Код ITЗагрузка примера кода…
Функция get_user_data создает соединение с базой данных и запрашивает пользователя. При отсутствии данных выбрасывается исключение. Тестирование этого кода с реальной базой данных потребует создания таблицы и заполнения её данными. Это усложняет процесс.
Создание мок-объекта для класса DatabaseConnection позволяет заменить реальное взаимодействие с базой данных на фиктивное поведение.
Содержимое файла tests/test_user_service.py:
Код ITЗагрузка примера кода…
Аннотация @patch заменяет класс DatabaseConnection внутри модуля app.user_service на мок-объект. Параметр mock_db_class в тестовой функции представляет собой этот замененный класс. Внутри теста создается экземпляр мок-объекта mock_instance. Метод fetch_user этого экземпляра настроен на возврат фиктивного словаря с именем "Bob".
Вызов get_user_data(1) использует мок вместо реального подключения к базе данных. Утверждение в конце теста (см. блок выше) проверяет возврат "Bob" и факт обращения к моку с нужным аргументом.
Такой подход гарантирует, что тест выполняется быстро и не зависит от наличия базы данных. Изменение логики базы данных не влияет на тесты сервиса, если интерфейс взаимодействия остается неизменным. Моки позволяют тестировать сложные сценарии, включая обработку ошибок и краевые случаи, без необходимости настройки реального окружения.
Что добавить в "первый" юнит-тест, чтобы он стал "рабочим"
После базового примера полезно сразу расширить набор проверок:
- Граничные значения —
0, отрицательные числа, большие числа. - Типы данных — строки,
None, смешанные типы — явно зафиксировать ожидаемую реакцию. - Ошибки: тест на ожидаемое исключение через
pytest.raises(...). - Именование: название теста описывает бизнес-ожидание, а не внутреннюю реализацию.
Пример дополнительных тестов:
Код ITЗагрузка примера кода…
Этот шаг превращает "демо на один assert" в основу для реального регресса.
Пирамида проверок и место unit-тестов
Unit-тесты дают быстрый сигнал о поломке логики и запускаются чаще всего: локально, в pre-commit и в CI. Они закрывают нижний уровень пирамиды тестирования — много, быстро, дёшево в поддержке.
При этом unit-тесты не заменяют интеграционные и сквозные проверки:
- unit проверяет функцию в изоляции;
- integration проверяет стык модулей и контрактов;
- E2E проверяет пользовательский путь целиком.
Чтобы увидеть полную картину, свяжите этот практикум с картой уровней и практик, интеграционным практикумом и материалом про E2E и системное тестирование.
Для E2E в браузере на том же pytest — фикстуры, pytest-playwright, Allure, Page Object и CI описаны в Playwright.
Частые проблемы в первых автотестах
- Тесты зависят от порядка запуска — убирайте общие изменяемые данные между тестами.
- Тесты используют "живую" БД и внешний API — заменяйте зависимости моками или тестовыми контейнерами.
- Тесты проверяют "слишком много сразу" — делите сценарии на атомарные проверки.
- Тесты флапают из-за времени и случайности — фиксируйте seed, мокайте время и случайные генераторы.
Эти практики сильно снижают стоимость поддержки автотестов уже с первых недель проекта.
CI-кейс — почему "локально зелёный", а в пайплайне красный
Частый сценарий:
- локально тест проходит;
- в CI падает из-за версии Python, зависимостей или порядка запуска;
- команда тратит время на ручной поиск расхождений.
Чтобы снизить такие падения:
- фиксируйте версии зависимостей в
requirements.txtили lock-файле; - запускайте тесты в чистом окружении перед push;
- включайте в CI тот же набор команд, который используете локально;
- добавляйте понятный вывод ошибок и отчёты о падении.
Для расширения практики изучите артефакты качества и порядок этапов тестирования.
Шаблон артефакта — gate для unit в CI
Пример минимальных правил merge:
Unit Gate (обязательный):
- Все unit-тесты: passed
- Flaky-тесты: 0
- Покрытие изменённых файлов: >= 80%
- Время шага unit: <= 5 минут
- При падении: merge blocked
Такой gate делает требования прозрачными и снижает споры "почему пайплайн красный".
Готовые поля для Jira и YouTrack
Issue Type: Task
Summary: [TEST] Обновить unit gate для модуля X
Description:
- Цель: стабилизировать unit-прогон в CI
- Объём: покрытие, flaky, тайминги
Definition of Done:
[ ] Unit gate обновлён
[ ] Порог покрытий зафиксирован
[ ] Документация CI обновлена
[ ] Прогон на main зелёный
Assignee/Reviewer: ...
Пример заполнения:
Issue Type: Task
Summary: [TEST] Усилить unit gate для billing-core
Description:
- Цель: снизить регрессии в расчётах скидок
- Объём: branch coverage, flaky, лимит времени шага
Definition of Done:
[x] Unit gate обновлён
[x] Порог покрытий зафиксирован на 85%
[x] Документация CI обновлена
[x] Прогон на main зелёный 5 запусков подряд
Assignee/Reviewer: qa.sdet / teamlead.backend
Навигация по разделу "Тестирование"
- Маршрут: О разделе · Резюме раздела · Карта уровней и практик (Unit / Integration / UI / E2E, TDD, BDD)
- Теория и процесс: Основы · Классификация · Жизненный цикл · Порядок этапов · Артефакты качества
- Уровни проверок: Unit · Integration · E2E, системное и UI · API · Тестовые дублёры · Покрытие кода · White-box · Мутационное тестирование
- Практика QA: Документация · Тест-дизайн · Ручное веб · SQL
- Автоматизация: Стратегия и пирамида · Каталог инструментов · Selenium · Playwright
- Практикум и углубление: Подготовка среды и создание первого теста · Проверка взаимодействия компонентов · Проверка пользовательского сценария · Проверка надежности под нагрузкой · Мобильное · Нагрузка · Безопасность · Самопроверка · Доп. материалы курса · Инструменты с низким кодом для тестирования · Тестирование нейроморфных систем