Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Coverage: контроль качества тестов
coverage_basics

Coverage: контроль качества тестов

pytest-cov установка, pytest --cov, чтение метрик, HTML-отчёты. Находим дыры в покрытии

У вас 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): 12
  • Miss — пропущено (не выполнено): 8
  • Cover — покрытие: 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 отчёт

Главная страница:

FileStatementsMissingCoverage
calculator.py120100%
user.py20575%
TOTAL32584%

Детальный просмотр файла:

# Зелёным — покрытые строки # Красным — непокрытые строки 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) == True

Line 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") == 80

Line 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

Встроенная поддержка:

  1. Run → Run with Coverage
  2. PyCharm автоматически покажет покрытие

Что вы изучили

  • pytest-cov — измерение покрытия кода
  • --cov-report=term-missing — какие строки пропущены
  • --cov-report=html — визуальные отчёты
  • Line vs Branch coverage — разные метрики
  • --cov-fail-under — минимальный порог
  • .coveragerc — конфигурация
  • Coverage targets — сколько покрытия достаточно

Проверьте свои знания

Вопросы ещё не добавлены

Вопросы для этой подтемы ещё не добавлены.

Далее: pytest.ini: конфигурация для команды