Перейти к содержимому
К программе курса
Pytest: Профессиональные инструменты
7 / 888%

Coverage thresholds и enforcement

15 минут

Pull request с новым кодом. Coverage упал с 85% до 70%. Код мержится. Через месяц — 50%. Как автоматически блокировать такое?

Цель: Настроить минимальные пороги покрытия и enforcement в CI.

Вы точно готовы?

Убедитесь, что умеете:

# Измерять coverage
pytest --cov=src --cov-report=html
 
# Читать метрики
# Name          Stmts   Miss  Cover
# mymodule.py      50     10    80%

Если coverage непонятен — вернитесь к pytest-junior урок 4.

Проблема: постепенное снижение покрытия

Без enforcement

# Week 1
pytest --cov=src
# Coverage: 85%
 
# Week 2 (добавлен код без тестов)
pytest --cov=src
# Coverage: 78%
 
# Week 4
pytest --cov=src
# Coverage: 65%
 
# Week 8
pytest --cov=src
# Coverage: 45%  ❌

Проблема: Покрытие падает незаметно, никто не замечает!

Решение: --cov-fail-under

Минимальный порог

pytest --cov=src --cov-fail-under=80

Если coverage < 80%:

---------- coverage: platform darwin, python 3.11 -----------
Name          Stmts   Miss  Cover
---------------------------------
src/app.py       50     15    70%
---------------------------------
TOTAL            50     15    70%
 
FAIL Required test coverage of 80% not reached. Total coverage: 70.00%

pytest завершается с exit code 1 — CI заблокирован!

Настройка в pytest.ini

# pytest.ini
 
[pytest]
addopts =
    --cov=src
    --cov-report=term-missing
    --cov-report=html
    --cov-fail-under=80  # Минимум 80%

Теперь просто:

pytest

✅ Всегда проверяет порог!

Разные пороги для разных команд

# pytest.ini (для локальной разработки)
 
[pytest]
addopts =
    --cov=src
    --cov-fail-under=70  # Мягкий порог локально
# .gitlab-ci.yml (для CI)
 
test:
  script:
    - pytest --cov-fail-under=80 # Строгий порог в CI

Per-module coverage

Проблема: общий coverage скрывает проблемы

Name                    Stmts   Miss  Cover
--------------------------------------------
src/core/auth.py           50      0   100%  ✅
src/core/payments.py       50     40    20%  ❌
--------------------------------------------
TOTAL                     100     40    60%  ⚠️

60% общего покрытия выглядит приемлемо, но payments — катастрофа!

.coveragerc: минимумы по модулям

# .coveragerc
 
[run]
source = src
omit =
    */tests/*
    */migrations/*
 
[report]
precision = 2
show_missing = True
 
# Минимальное покрытие для разных модулей
fail_under = 80
 
[coverage:paths]
source =
    src/
 
# Per-module thresholds (через плагин)

Плагин для per-module:

pip install coverage-conditional-plugin
# .coveragerc
 
[coverage:run]
plugins = coverage_conditional_plugin
 
[coverage:paths:conditional]
# Критичные модули — 90%
src/payments/ = 90
src/auth/ = 90
 
# Обычные модули — 80%
src/api/ = 80
 
# UI/CLI — 60%
src/cli/ = 60

Интеграция в CI

GitLab CI

# .gitlab-ci.yml
 
test:
  stage: test
  script:
    - pip install -r requirements.txt
    - pytest --cov=src --cov-fail-under=80 --cov-report=xml
    - coverage report --fail-under=80
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

GitHub Actions

# .github/workflows/test.yml
 
- name: Run tests with coverage
  run: |
    pytest --cov=src --cov-fail-under=80 --cov-report=xml
 
- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v3
  with:
    files: ./coverage.xml
    fail_ci_if_error: true

Quality gates с Codecov

# codecov.yml
 
coverage:
  status:
    project:
      default:
        target: 80% # Минимум для всего проекта
        threshold: 1% # Допустимое падение
    patch:
      default:
        target: 90% # Новый код должен быть лучше!

Результат: PR блокируется если:

  • Общее покрытие < 80%
  • Новый код покрыт < 90%
  • Coverage упал > 1%

Практический workflow (bonus)

Локальная разработка

# Быстрая проверка (без строгого порога)
pytest --cov=src
 
# Перед commit (строгий порог)
pytest --cov=src --cov-fail-under=75

Pre-commit hook

# .git/hooks/pre-commit
 
#!/bin/bash
pytest --cov=src --cov-fail-under=75 --cov-report=term-missing
 
if [ $? -ne 0 ]; then
    echo "❌ Coverage is below 75%, commit blocked!"
    exit 1
fi
 
echo "✅ Coverage check passed"

CI pipeline

stages:
  - test
  - quality-gate
  - deploy
 
unit-tests:
  stage: test
  script:
    - pytest --cov=src --cov-fail-under=70
 
coverage-check:
  stage: quality-gate
  script:
    - pytest --cov=src --cov-fail-under=80
    - coverage html
  artifacts:
    paths:
      - htmlcov/
 
deploy:
  stage: deploy
  dependencies:
    - coverage-check # Не задеплоим если coverage < 80%

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

  • --cov-fail-under — минимальный порог покрытия
  • pytest.ini — автоматическая проверка
  • Per-module — разные пороги для разных модулей
  • CI integration — блокировка плохих PR
  • Quality gates — Codecov, SonarQube
  • Workflow — локально vs CI vs pre-commit

Следующий урок

Поздравляю! Вы изучили 7 из 8 уроков pytest-professional-tools!

Теперь пора собрать всё вместе и превратить учебный проект в production-ready!

Переходите к уроку 7: Production-ready проект

В следующем уроке вы:

  • Применяете всё изученное на практике
  • Создаёте production-ready структуру
  • Настраиваете xdist, coverage, плагины
  • Готовите проект к команде

Устранение неисправностей

Проверьте exit code команды.

pytest --cov=src --cov-fail-under=80
echo $?  # Должен быть 1 если coverage < 80%

В CI нельзя глушить ошибку:

# ✅ ПРАВИЛЬНО
script:
- pytest --cov-fail-under=80

# ❌ НЕПРАВИЛЬНО (игнорирует exit code)

script:

- pytest --cov-fail-under=80 || true

Coverage — не единственный критерий.

  • Добавьте mutation testing (mutpy/pytest-mutagen)
  • Проводите code review по сценарию
  • Запускайте статанализ (mypy, ruff/flake8)

Проверяйте покрытие дельты, а не только общее.

coverage xml
diff-cover coverage.xml --fail-under=90 # сравнение с main

Поднимайте порог поэтапно.

# Week 1
--cov-fail-under=50

# Week 4

--cov-fail-under=60

# Week 8

--cov-fail-under=70

# Week 12

--cov-fail-under=80
Coverage thresholds и enforcement — Pytest: Профессиональные инструменты — Potapov.me