> Как организовать юнит-тесты в Python? (Python)
Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы
Компании: praktika.ai, Исходный Код
Стек: Python
> Пример ответа
Короткий ответ
Организация юнит-тестов в Python строится вокруг трёх уровней: структура файлов (папка tests/ рядом с пакетом), инструменты (pytest, unittest, mock) и соглашения о нейминге (test_*.py, функции test_*). Для senior-уровня важно не просто писать тесты, а проектировать их: изолировать внешние зависимости через dependency injection и unittest.mock, использовать фикстуры для подготовки окружения, параметризацию для покрытия кейсов. Тесты должны быть быстрыми, детерминированными и независимыми, чтобы их можно было запускать параллельно и в CI.
Подробное объяснение
Юнит-тесты проверяют поведение отдельной единицы кода - функции, класса, метода - в изоляции от внешних систем (БД, сеть, файловая система). В Python стандартный подход - unittest из стандартной библиотеки, но де-факто индустриальный стандарт - pytest с его фикстурами, параметризацией и плагинами.
Ключевые принципы организации:
- Структура: тесты лежат в отдельной директории
tests/, зеркалящей структуру пакета. Например,myapp/services/user_service.py→tests/services/test_user_service.py. Это упрощает навигацию и позволяет запускать точечные тесты. - Изоляция: каждый тест создаёт свои данные и не зависит от порядка выполнения. Для этого используются фикстуры с
scope="function"(по умолчанию) илиscope="module"/"session"для тяжёлых ресурсов. - Моки и стабы: внешние вызовы (HTTP, БД, очереди) заменяются на
unittest.mock.Mockилиpytest-mock. Важно мокать на границе системы - там, где код взаимодействует с внешним миром, а не внутри бизнес-логики. - Параметризация:
@pytest.mark.parametrizeпозволяет прогнать один тест с разными входными данными, включая граничные случаи и ошибки. - Покрытие: измеряется через
pytest-cov, но цель не 100%, а покрытие критичных веток и обработки ошибок.
Для senior-уровня добавляется проектирование тестов как части архитектуры: тесты пишутся до или вместе с кодом (TDD), они являются спецификацией поведения, и их поддержка - такая же задача, как поддержка продакшн-кода. Это означает: тесты должны быть читаемыми, не дублировать логику, использовать доменные термины в названиях.
На практике
На практике организация выглядит так:
- Выбор фреймворка:
pytest- основной,unittest- если проект legacy или нужен встроенный runner. - Конфигурация: файл
pytest.iniилиpyproject.tomlс настройками (testpaths,addopts,markers). - Фикстуры: выносятся в
conftest.pyна уровне директории, чтобы переиспользовать между модулями. Тяжёлые ресурсы (БД, Redis) - в session-scoped фикстуры с teardown. - Маркеры:
@pytest.mark.slow,@pytest.mark.integrationдля разделения быстрых unit-тестов и медленных интеграционных. В CI запускаются только unit-тесты на каждый коммит, интеграционные - на merge request. - Запуск:
pytest tests/ -x -qдля быстрой проверки,pytest --cov=myapp --cov-report=term-missingдля отчёта о покрытии.
Для изоляции от внешних сервисов часто используется паттерн "порт-адаптер": бизнес-логика зависит от абстракций (интерфейсов), а в тестах подставляются фейковые реализации. Это лучше, чем мокать конкретные библиотеки, потому что тесты не ломаются при смене HTTP-клиента или драйвера БД.
Пример кода
PYTHON# myapp/services/user_service.pyfrom dataclasses import dataclassfrom typing import Protocolclass UserRepository(Protocol):def get_by_id(self, user_id: int) -> dict | None: ...@dataclassclass UserService:repo: UserRepositorydef get_username(self, user_id: int) -> str:user = self.repo.get_by_id(user_id)if user is None:raise ValueError(f"User {user_id} not found")return user["name"]# tests/conftest.pyimport pytestfrom myapp.services.user_service import UserServiceclass FakeRepo:def __init__(self, data: dict[int, dict]):self._data = datadef get_by_id(self, user_id: int) -> dict | None:return self._data.get(user_id)@pytest.fixturedef service():repo = FakeRepo({1: {"name": "Alice"}})return UserService(repo)# tests/services/test_user_service.pyimport pytestdef test_get_username_existing_user(service):assert service.get_username(1) == "Alice"def test_get_username_missing_user(service):with pytest.raises(ValueError, match="not found"):service.get_username(42)@pytest.mark.parametrize("user_id,expected", [(1, "Alice"), (2, "Bob")])def test_get_username_parametrized(user_id, expected):repo = FakeRepo({1: {"name": "Alice"}, 2: {"name": "Bob"}})svc = UserService(repo)assert svc.get_username(user_id) == expected
Как отвечать на собеседовании
Начни с краткого определения юнит-теста и его цели - проверка изолированного поведения. Затем переходи к структуре: tests/ рядом с пакетом, нейминг test_*. Упомяни pytest как основной инструмент, но покажи, что знаешь unittest и его ограничения (больше boilerplate, нет фикстур из коробки). Подчеркни важность изоляции через dependency injection и моки - это ключевой момент для senior. Расскажи про фикстуры и их scope, параметризацию, маркеры для разделения тестов. Если спросят про покрытие - скажи, что гнаться за 100% не нужно, важнее покрыть критичные ветки и обработку ошибок. Заверши примером из практики: как ты организовывал тесты в реальном проекте, какие проблемы решал (например, параллельный запуск, тесты, которые падали из-за порядка выполнения).
Что проверяет интервьюер
Интервьюер оценивает:
- Понимание разницы между unit, integration и e2e тестами - и умение объяснить, почему unit-тесты должны быть быстрыми и изолированными.
- Умение проектировать тестируемый код: dependency injection, порты-адаптеры, отсутствие скрытых глобальных состояний.
- Практическое знание инструментов:
pytestфикстуры,mock,parametrize,conftest.py. - Навыки отладки тестов: как найти flaky тест, как изолировать проблему, как использовать
--pdbили-x. - Понимание CI-процессов: какие тесты запускать на каждый коммит, какие - на merge, как ускорить прогон (параллелизм через
pytest-xdist).
Для senior-позиции важно показать, что тесты - это не обязанность, а инструмент проектирования: они влияют на архитектуру кода, заставляют делать код слабо связанным и легко заменяемым.
Типичные ошибки
- Тесты зависят от порядка выполнения - например, используют общие данные без очистки. Решение: фикстуры с teardown, изоляция данных на каждый тест.
- Мокают слишком много - мокают внутренние вызовы вместо внешних границ, из-за чего тесты перестают проверять реальное поведение и ломаются при рефакторинге.
- Тестируют реализацию, а не поведение - проверяют, что метод вызван с определёнными аргументами, вместо проверки результата. Это делает тесты хрупкими.
- Игнорируют обработку ошибок - тестируют только happy path, а исключения и граничные случаи остаются непокрытыми.
- Тяжёлые тесты - поднимают БД или делают реальные HTTP-запросы в unit-тестах. Это делает прогон медленным и недетерминированным.
- Дублирование логики в тестах - например, переписывают алгоритм внутри теста, чтобы "проверить" его. Тест должен проверять спецификацию, а не повторять реализацию.
- Отсутствие структуры - все тесты в одном файле, нет разделения на модули, нет
conftest.pyдля общих фикстур. Это усложняет поддержку и навигацию.
> Похожие задачи по Python
Что такое git flow
Как тестировать миграции базы данных?
Что такое нефункциональное тестирование и какие виды оно включает?
Как оптимизировать SQL запросы
> Похожие задачи по backend
Что происходит внутри dict при добавлении нового элемента
Как тестировать миграции базы данных?
Что такое нефункциональное тестирование и какие виды оно включает?
Как оптимизировать SQL запросы
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью