Coverage: контроль качества тестов
У вас 50 тестов. Класс PaymentProcessor имеет 10 методов. Сколько методов покрыто тестами? 3? 7? Все 10? Как узнать?
Цель: Научиться измерять покрытие кода тестами с помощью coverage.
Вы точно готовы?
Убедитесь, что умеете:
# Писать тесты с моками
from unittest.mock import patch
@patch("module.requests")
def test_api(mock_requests):
mock_requests.get.return_value.json.return_value = {"data": 123}Если мокирование непонятно — вернитесь к предыдущим урокам (0-3).
Проблема: непокрытый код
Код с тестами
# src/calculator.py
class Calculator:
def add(self, a, b):
"""Сложение"""
return a + b
def subtract(self, a, b):
"""Вычитание"""
return a - b
def divide(self, a, b):
"""Деление"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def power(self, a, b):
"""Возведение в степень"""
if b < 0:
raise ValueError("Negative exponent not supported")
return a ** b
# tests/test_calculator.py
def test_add():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_subtract():
calc = Calculator()
assert calc.subtract(5, 3) == 2Вопросы:
- ❓ Метод
divide()покрыт тестами? - ❓ Проверка
if b == 0протестирована? - ❓ Метод
power()вообще используется?
Проблема: Неочевидно какой код НЕ покрыт тестами!
Решение: pytest-cov
Установка
pip install pytest-covБазовый запуск
# Запускаем тесты с измерением покрытия
pytest --cov=src tests/Разбор команды:
pytest— запуск тестов--cov=src— измерить покрытие для директорииsrc/(путь относительно корня проекта)tests/— запустить тесты из директорииtests/
Результат:
========================= test session starts ==========================
collected 2 items
tests/test_calculator.py .. [100%]
---------- coverage: platform darwin, python 3.11 -----------
Name Stmts Miss Cover
-----------------------------------------
src/calculator.py 12 8 33%
-----------------------------------------
TOTAL 12 8 33%
========================== 2 passed in 0.12s ===========================Что это значит:
Stmts— всего строк кода (statements): 12Miss— пропущено (не выполнено): 8Cover— покрытие: 33%
❌ Только 33% кода покрыто тестами!
Детальный отчёт: какие строки пропущены
pytest --cov=src --cov-report=term-missing tests/Разбор аргументов:
--cov=src— измерить покрытие кода в директорииsrc/--cov-report=term-missing— показать отчёт в терминале (term) с номерами пропущенных строк (missing)tests/— где искать тесты
Результат:
Name Stmts Miss Cover Missing
---------------------------------------------------
src/calculator.py 12 8 33% 10-11, 15-16, 20-24
---------------------------------------------------
TOTAL 12 8 33%✅ Теперь видно КАКИЕ строки не покрыты: 10-11, 15-16, 20-24
Смотрим код:
# src/calculator.py
class Calculator:
def add(self, a, b):
return a + b # ✅ Покрыто
def subtract(self, a, b):
return a - b # ✅ Покрыто
def divide(self, a, b): # ❌ Строка 10 — не покрыта
if b == 0: # ❌ Строка 11 — не покрыта
raise ValueError(...)
return a / b
def power(self, a, b): # ❌ Строки 15-16 — не покрыты
if b < 0:
raise ValueError(...)
return a ** b # ❌ Строки 20-24 — не покрыты✅ Понятно что нужно добавить тесты для divide() и power()!
Добавляем тесты
# tests/test_calculator.py
def test_divide():
calc = Calculator()
assert calc.divide(10, 2) == 5
def test_divide_by_zero():
calc = Calculator()
with pytest.raises(ValueError):
calc.divide(10, 0)
def test_power():
calc = Calculator()
assert calc.power(2, 3) == 8
def test_power_negative():
calc = Calculator()
with pytest.raises(ValueError):
calc.power(2, -1)Запускаем снова:
pytest --cov=src --cov-report=term-missing tests/Результат:
Name Stmts Miss Cover
-----------------------------------------
src/calculator.py 12 0 100%
-----------------------------------------
TOTAL 12 0 100%✅ 100% покрытие!
HTML отчёты: визуализация
Генерация HTML отчёта
pytest --cov=src --cov-report=html tests/Разбор аргументов:
--cov=src— измерить покрытие дляsrc/--cov-report=html— сгенерировать HTML отчёт (вместо текстового в терминале)tests/— директория с тестами
Результат:
Coverage HTML written to dir htmlcovОткрываем в браузере:
open htmlcov/index.html # macOS
# или
xdg-open htmlcov/index.html # Linux
# или
start htmlcov/index.html # WindowsЧто показывает HTML отчёт
Главная страница:
| File | Statements | Missing | Coverage |
|---|---|---|---|
| calculator.py | 12 | 0 | 100% |
| user.py | 20 | 5 | 75% |
| TOTAL | 32 | 5 | 84% |
Детальный просмотр файла:
# Зелёным — покрытые строки
# Красным — непокрытые строки
class Calculator:
def add(self, a, b):
return a + b # ✅ Зелёная строка
def divide(self, a, b):
if b == 0: # ❌ Красная строка — не покрыто!
raise ValueError("Cannot divide by zero")
return a / b # ✅ Зелёная✅ Наглядно видно что именно не покрыто!
Чтение метрик: Lines vs Branches
Line coverage (по умолчанию)
def is_adult(age):
if age >= 18: # ✅ Строка выполнена
return True
else: # ❌ Строка НЕ выполнена
return False
# Тест
def test_adult():
assert is_adult(25) == TrueLine coverage: 75% (3 из 4 строк)
Проблема: else ветка не протестирована!
Branch coverage (строже)
pytest --cov=src --cov-branch tests/Разбор аргументов:
--cov=src— измерить покрытие дляsrc/--cov-branch— включить branch coverage (покрытие веток условий)tests/— директория с тестами
Branch coverage проверяет ВСЕ ветки (if/else):
Name Stmts Miss Branch BrPart Cover
-----------------------------------------------------
src/user.py 4 1 2 1 60%
-----------------------------------------------------Branch— всего веток: 2 (if True, if False)BrPart— частично покрыто: 1 (только if True)Cover— 60% (не 75%!)
✅ Branch coverage строже и находит больше дыр!
Практический пример
# src/discount.py
def calculate_discount(price, user_type):
"""Расчёт скидки"""
if user_type == "vip":
discount = 0.2
elif user_type == "regular":
discount = 0.1
else:
discount = 0
return price * (1 - discount)
# tests/test_discount.py
def test_vip_discount():
assert calculate_discount(100, "vip") == 80Line coverage: 6/8 = 75%
Branch coverage: 1/3 веток = 33%
Непокрыто:
elif user_type == "regular"— не провереноelse— не проверено
✅ Branch coverage показывает реальную проблему!
Настройка coverage
.coveragerc: конфигурация
# .coveragerc (в корне проекта)
[run]
source = src
omit =
*/tests/*
*/migrations/*
*/__init__.py
[report]
precision = 2
show_missing = True
skip_covered = False
[html]
directory = htmlcovТеперь можно запускать просто:
pytest --covИсключение кода из coverage
# src/config.py
def get_database_url():
"""Получает URL БД"""
if os.getenv("ENV") == "production": # pragma: no cover
return "postgresql://..."
else:
return "sqlite:///:memory:"✅ pragma: no cover — строка игнорируется в coverage!
Когда использовать:
- Код только для production
- Defensive programming (невозможные условия)
- Debug код
Minimum coverage: fail если < 80%
pytest --cov=src --cov-fail-under=80 tests/Разбор аргументов:
--cov=src— измерить покрытие дляsrc/--cov-fail-under=80— завершить с ошибкой (exit code 1), если покрытие меньше 80%tests/— директория с тестами
Если coverage < 80%:
FAIL Required test coverage of 80% not reached. Total coverage: 75.00%✅ Можно использовать в CI для контроля качества!
Практический пример: реальный проект
# Структура проекта
my-project/
├── src/
│ ├── __init__.py
│ ├── api/
│ │ ├── client.py
│ │ └── auth.py
│ └── services/
│ ├── user_service.py
│ └── payment_service.py
├── tests/
│ ├── test_api.py
│ └── test_services.py
├── .coveragerc
└── pytest.iniЗапуск с отчётом:
# Полный отчёт
pytest --cov=src --cov-branch --cov-report=html --cov-report=term-missingРазбор аргументов:
--cov=src— измерить покрытие дляsrc/--cov-branch— включить branch coverage (проверка всех веток if/else)--cov-report=html— создать HTML отчёт вhtmlcov/--cov-report=term-missing— показать в терминале с номерами непокрытых строк
💡 Можно комбинировать несколько форматов отчётов!
Результат:
Name Stmts Miss Branch BrPart Cover Missing
---------------------------------------------------------------------------
src/__init__.py 0 0 0 0 100%
src/api/client.py 45 5 12 2 88% 23-25, 67
src/api/auth.py 32 0 10 0 100%
src/services/user_service.py 67 12 18 3 80% 45-50, 89-95
src/services/payment_service.py 89 23 22 5 71% Multiple lines
---------------------------------------------------------------------------
TOTAL 233 40 62 10 81%Анализ:
- ✅
api/auth.py— 100%, отлично! - ⚠️
api/client.py— 88%, почти идеально - ⚠️
user_service.py— 80%, приемлемо - ❌
payment_service.py— 71%, нужно добавить тесты!
Открываем HTML отчёт:
open htmlcov/index.htmlНаходим непокрытые строки в payment_service.py:
- Строки 45-50: обработка ошибок Stripe (не протестирована)
- Строки 89-95: рефанд платежей (не протестирован)
✅ Добавляем тесты для этих сценариев!
Coverage targets: сколько нужно?
Рекомендации по покрытию
| Coverage | Оценка | Рекомендация |
|---|---|---|
| < 50% | ❌ Очень низкое | Критично добавить тесты |
| 50-70% | ⚠️ Низкое | Добавить тесты для критичного кода |
| 70-85% | ✅ Хорошо | Достаточно для большинства проектов |
| 85-95% | ✅✅ Отлично | Высокое качество |
| > 95% | ⚠️ Слишком высокое? | Может быть избыточно |
Когда 100% НЕ нужно
# ❌ Бессмысленно тестировать
class Config:
DATABASE_URL = "postgresql://..."
API_KEY = "secret"
# ❌ Бессмысленно тестировать
def __repr__(self):
return f"<User {self.name}>"
# ❌ Бессмысленно тестировать
if __name__ == "__main__": # pragma: no cover
main()Рекомендация:
- Критичный код (payment, auth): 90-100%
- Бизнес-логика: 80-90%
- UI/Views: 60-70%
- Config/Constants: можно пропустить
Интеграция с IDE (bonus)
VS Code
Установите расширение: Coverage Gutters
# Генерируем XML отчёт
pytest --cov=src --cov-report=xml
# Расширение автоматически подсветит покрытие в редактореРазбор аргументов:
--cov=src— измерить покрытие дляsrc/--cov-report=xml— сгенерировать XML отчёт (для IDE и CI/CD)
Результат:
- ✅ Зелёные линии слева — покрыто
- ❌ Красные линии — не покрыто
PyCharm
Встроенная поддержка:
- Run → Run with Coverage
- PyCharm автоматически покажет покрытие
Что вы изучили
- pytest-cov — измерение покрытия кода
- --cov-report=term-missing — какие строки пропущены
- --cov-report=html — визуальные отчёты
- Line vs Branch coverage — разные метрики
- --cov-fail-under — минимальный порог
- .coveragerc — конфигурация
- Coverage targets — сколько покрытия достаточно
Следующий урок
Отлично! Теперь вы умеете измерять покрытие тестами. Но как настроить pytest для всей команды? Как задать маркеры, testpaths, и другие опции?
Переходите к уроку 5: pytest.ini: конфигурация для команды
В следующем уроке вы узнаете:
pytest.iniдля настройки проекта- Регистрация маркеров
testpathsдля поиска тестовaddoptsдля опций по умолчанию- Настройка для CI/CD
Устранение неисправностей
Убедитесь что pytest-cov установлен:
pip install pytest-cov
pytest --version # Должен показать pytest-cov в списке плагиновПроверьте путь в --cov=src:
# ✅ Правильно (для src/ директории)
pytest --cov=src tests/
# ❌ Неправильно
pytest --cov=tests tests/ # Измеряет покрытие ТЕСТОВ, не кода!Используйте .coveragerc:
[run]
omit =
_/tests/_
_/migrations/_
_/**pycache**/_- Используйте
pragma: no coverдля невозможного кода - Установите реалистичный порог:
--cov-fail-under=80 - Помните: 80-90% обычно достаточно!
Удалите старый отчёт:
rm -rf htmlcov .coverage
pytest --cov=src --cov-report=html