## Вопрос, на который отвечает покрытие
У вас есть тест-сьют -- но какую часть реального кода он выполняет? Функция с тремя ветками может тестироваться только по основному сценарию, оставляя две ветки полностью нетронутыми. **Покрытие** измеряет, какие строки (и какие ветки) были реально выполнены при прогоне тестов.
Покрытие -- диагностический инструмент: он показывает, где ваши тесты не смотрят. Он не говорит, *хорошие* ли ваши тесты.
## pytest-cov: покрытие интегрировано в pytest
`pytest-cov` -- плагин, оборачивающий `coverage.py` и запускающий его вместе с тестами:
```bash
pip install pytest-cov
```
Запустить тесты с измерением покрытия:
```bash
pytest --cov=src tests/
```
`--cov=src` указывает пакет или директорию для измерения. По завершении прогона автоматически выводится отчёт о покрытии.
## Чтение терминального отчёта
```
---------- coverage: platform linux, python 3.11 ----------
Name Stmts Miss Cover
------------------------------------------
src/products.py 42 3 93%
src/auth.py 28 8 71%
src/utils.py 15 0 100%
------------------------------------------
TOTAL 85 11 87%
```
- **Stmts** -- всего исполняемых инструкций в файле
- **Miss** -- инструкции, которые так и не были достигнуты за прогон
- **Cover** -- процент инструкций Stmts, которые были выполнены
Чтобы увидеть *какие именно* строки пропущены, добавьте `--cov-report=term-missing`:
```bash
pytest --cov=src --cov-report=term-missing tests/
```
Вывод включает колонку `Missing`:
```
src/auth.py 28 8 71% 45-52, 78
```
Строки 45-52 и 78 никогда не выполнялись. Откройте файл -- возможно, там блок обработки ошибок, для которого нужен тест "что если база данных недоступна".
## HTML-отчёт: лучший способ просмотра
```bash
pytest --cov=src --cov-report=html tests/
```
Генерирует `htmlcov/index.html`. Откройте в браузере: выполненные строки зелёные, невыполненные красные. Кликните любой файл, чтобы увидеть точно, какие строки не покрыты в контексте.
## Покрытие строк vs покрытие ветвей
**Покрытие строк** фиксирует, выполнилась ли строка вообще. Но одна инструкция `if` содержит два пути:
```python
def discount(price, is_member):
if is_member: # <- выполнилась
return price * 0.9 # <- выполнилась (ветка True)
return price # <- НИКОГДА не выполнялась (ветка False)
```
Если тесты вызывают только `discount(100, True)`, покрытие строк показывает 100% -- каждая строка выполнилась. Но путь `False` так и не был протестирован.
**Покрытие ветвей** это улавливает. Включите его:
```bash
pytest --cov=src --cov-branch tests/
```
Теперь отчёт отслеживает каждую ветку каждого `if`, `for`, `while`, `try/except` и условного выражения. Ветка, которая никогда не выполнялась, отображается как пробел.
## Почему 100% покрытие строк -- не цель
Можно достичь 100% покрытия строк тестами, которые ничего не проверяют. Можно иметь 75% покрытие с тестами, улавливающими каждый реальный баг. Число -- подсказка, а не цель.
Правильное использование покрытия:
- **Найти пробелы** -- какие нетривиальные пути кода не имеют тестов? Они важны?
- **Установить минимальную планку** -- `--cov-fail-under=80` в CI предотвращает случайные регрессии (падение покрытия с 85% до 60% после рефакторинга -- тревожный сигнал)
- **Выявить мёртвый код** -- строка, которая *всегда* не покрыта, может быть недостижима и безопасна для удаления
Не гонитесь за числом. Гонитесь за осмысленными тестами.
## Исключение кода из покрытия
Не всё стоит измерять. Файлы конфигурации, заглушки миграций, утилиты отладки -- исключите их:
**Через `.coveragerc`:**
```ini
[run]
omit =
tests/*
setup.py
*/migrations/*
[report]
exclude_lines =
if __name__ == .__main__.:
raise NotImplementedError
```
**Инлайн через pragma:**
```python
if __name__ == '__main__': # pragma: no cover
main()
```
Комментарий pragma исключает эту строку из всех отчётов о покрытии. Используйте экономно -- только для кода, который действительно не имеет смысла тестировать.
## Тестируемый код
```python
# src/pricing.py
def calculate_price(base_price, is_member, coupon_code=None):
price = base_price
if is_member:
price *= 0.9 # скидка 10% для членов
if coupon_code == 'SAVE20':
price *= 0.8 # дополнительная скидка 20%
elif coupon_code is not None:
raise ValueError(f'Unknown coupon: {coupon_code}')
return round(price, 2)
```
## Частичный тест-сьют (намеренно неполный)
```python
# tests/test_pricing.py
from src.pricing import calculate_price
def test_no_discount():
assert calculate_price(100, is_member=False) == 100.0
def test_member_discount():
assert calculate_price(100, is_member=True) == 90.0
```
## Запуск покрытия и чтение отчёта
```bash
pytest --cov=src --cov-report=term-missing tests/
```
```
Name Stmts Miss Cover Missing
-------------------------------------------------
src/pricing.py 9 2 78% 10, 12
```
78% -- ветки купонного кода так и не были протестированы. Строки 10 и 12 -- это блок `price *= 0.8` и блок `raise ValueError`.
## Добавление покрытия ветвей
```bash
pytest --cov=src --cov-branch --cov-report=term-missing tests/
```
```
Name Stmts Miss Branch BrPart Cover
src/pricing.py 9 2 6 2 64%
```
Ещё ниже -- непротестированные ветки внутри условий купона учитываются в покрытии отдельно.
## Закрытие пробелов
```python
import pytest
def test_coupon_save20():
assert calculate_price(100, is_member=False, coupon_code='SAVE20') == 80.0
def test_member_and_coupon():
assert calculate_price(100, is_member=True, coupon_code='SAVE20') == pytest.approx(72.0) # float-safe
def test_invalid_coupon():
with pytest.raises(ValueError, match='Unknown coupon: INVALID'):
calculate_price(100, is_member=False, coupon_code='INVALID')
```
После добавления этих тестов покрытие для этого файла достигает 100%.
## HTML-отчёт
```bash
pytest --cov=src --cov-report=html tests/
# затем открыть htmlcov/index.html в браузере
```
Красные строки -- не покрыты; зелёные -- выполнены. Кликайте по файлам, чтобы увидеть точно, что было пропущено.
## .coveragerc: исключение файлов и строк
```ini
# .coveragerc
[run]
omit =
tests/*
setup.py
[report]
exclude_lines =
if __name__ == .__main__.:
raise NotImplementedError
\.\.\.
[html]
directory = htmlcov
```
## Установка минимума в CI
```bash
pytest --cov=src --cov-fail-under=80 tests/
```
Завершается с кодом 2, если общее покрытие опускается ниже 80%. CI интерпретирует это как падение тестов. Это автоматически улавливает ситуацию "кто-то добавил код и забыл написать тесты".
## pragma: no cover
```python
def _debug_dump(obj): # pragma: no cover
'''Утилита разработчика -- не имеет смысла тестировать.'''
import pprint
pprint.pprint(vars(obj))
```
Функция не будет отображаться в статистике покрытия. Используйте это редко -- только для кода, который действительно нельзя осмысленно юнит-тестировать (утилиты отладки, блоки `__main__`, абстрактные заглушки).
## Объединение флагов для полной CI-команды
```bash
pytest --cov=src --cov-branch --cov-report=term-missing --cov-fail-under=75 tests/
```
Создайте `src/calculator.py` с тремя функциями: `add(a, b)`, `divide(a, b)` (выбрасывает `ZeroDivisionError` при `b == 0`) и `absolute_value(x)`. Создайте `tests/test_calc.py` с единственным тестом, тестирующим только `add()`. Запустите `pytest --cov=src --cov-report=term-missing tests/` и прочитайте вывод. Определите, какие функции не покрыты и какие номера строк перечислены в колонке Missing.
# src/calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ZeroDivisionError('cannot divide by zero')
return a / b
def absolute_value(x):
if x < 0:
return -x
return x
# tests/test_calc.py
from src.calculator import add
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
# Запустить: pytest --cov=src --cov-report=term-missing tests/
# Каков процент покрытия?
# Какие строки появляются в колонке Missing?
Решение
# src/calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ZeroDivisionError('cannot divide by zero')
return a / b
def absolute_value(x):
if x < 0:
return -x
return x
# tests/test_calc.py
from src.calculator import add
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
# Ожидаемый отчёт (приблизительно -- точные номера строк зависят от пустых строк в файле):
# Name Stmts Miss Cover Missing
# src/calculator.py <N> <Miss> ~40% <строки в divide() и absolute_value()>
#
# Колонка Missing покажет строки тела divide() и absolute_value().
# Откройте файл и проверьте: перечисленные строки должны быть внутри этих двух функций.
# Покрытие: ~40% -- выполняется только add().
02
Сгенерировать HTML-отчёт и написать недостающий тест
Используя `src/calculator.py` из упражнения 1: запустите `pytest --cov=src --cov-report=html tests/` для генерации `htmlcov/index.html`. Откройте в браузере (или используйте `--cov-report=term-missing` для нахождения строк). Напишите тесты для `divide()` и `absolute_value()` -- включая путь ошибки и обе ветки инструкции `if`. Запустите покрытие снова и убедитесь, что достигнуто 100%.
# tests/test_calc.py (расширить от упражнения 1)
from src.calculator import add, divide, absolute_value
import pytest
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
def test_divide():
# покрыть как нормальный путь, так и путь с ZeroDivisionError
pass
def test_absolute_value():
# покрыть обе ветки: отрицательный вход и положительный вход
pass
# Запустить: pytest --cov=src --cov-report=html tests/
# Открыть htmlcov/index.html -- красные строки не покрыты
Решение
# tests/test_calc.py
from src.calculator import add, divide, absolute_value
import pytest
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
def test_divide_normal():
assert divide(10, 2) == 5.0
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_absolute_value_negative():
assert absolute_value(-5) == 5
def test_absolute_value_positive():
assert absolute_value(3) == 3
# После добавления этих тестов:
# src/calculator.py 9 0 100%
# Все красные строки должны стать зелёными в HTML-отчёте.
03
Установить минимум покрытия через --cov-fail-under
Используя частичный тест-сьют из упражнения 1 (только `test_add`): запустите `pytest --cov=src --cov-fail-under=80 tests/` и наблюдайте код выхода -- он должен быть 2, а не 0. Обратите внимание на сообщение об ошибке в выводе. Затем добавьте полные тесты из упражнения 2 и запустите снова -- должен пройти с кодом выхода 0. Добавьте флаг в `pytest.ini` через `addopts`, чтобы сделать его постоянным.
# Запуск 1 -- только с test_add (ожидать код выхода 2, покрытие ~33%):
# pytest --cov=src --cov-fail-under=80 tests/
# Запуск 2 -- после добавления всех тестов (ожидать код выхода 0, покрытие 100%):
# pytest --cov=src --cov-fail-under=80 tests/
# Для постоянства добавить в pytest.ini:
# [pytest]
# addopts = --cov=src --cov-report=term-missing --cov-fail-under=80
Решение
# pytest.ini
# [pytest]
# addopts = --cov=src --cov-report=term-missing --cov-fail-under=80
# Вывод запуска 1 (частичные тесты):
# FAIL Required test coverage of 80% not reached. Total coverage: 33.33%
# (код выхода 2)
# Вывод запуска 2 (полный тест-сьют):
# Required test coverage of 80% reached. Total coverage: 100.00%
# (код выхода 0)
# addopts применяется к каждому запуску pytest автоматически -- локальная и CI
# среды используют одинаковые настройки без необходимости помнить флаги.
Создайте файл `.coveragerc`, исключающий `tests/` и `setup.py` из измерения, и исключающий строки, совпадающие с `"if __name__ == .__main__.:"`. Создайте `setup.py` с несколькими строками и `src/main.py` с функцией `run()` и блоком `if __name__ == "__main__":`. Запустите покрытие и убедитесь, что эти файлы и строки не появляются в отчёте.
# .coveragerc (создать в корне проекта)
[run]
omit =
# добавить: tests/*
# добавить: setup.py
[report]
exclude_lines =
# добавить паттерн guard __main__
# setup.py
from setuptools import setup
setup(name='myproject', version='0.1')
# src/main.py
def run():
print('running')
if __name__ == '__main__':
run()
# Запустить: pytest --cov=src tests/
# setup.py и строка __main__ не должны появляться в отчёте
Решение
# .coveragerc
[run]
omit =
tests/*
setup.py
[report]
exclude_lines =
if __name__ == .__main__.:
raise NotImplementedError
[html]
directory = htmlcov
# setup.py
from setuptools import setup
setup(name='myproject', version='0.1')
# src/main.py
def run():
print('running')
if __name__ == '__main__':
run()
# Запустить: pytest --cov=src tests/
# setup.py не появится; строка __main__ исключена из статистики
Добавьте функцию `debug_print(a, b)` в `src/calculator.py`, которая выводит оба аргумента (два оператора print). Добавьте `# pragma: no cover` к строке определения функции. Запустите `pytest --cov=src --cov-report=term-missing tests/` с полным тест-сьютом из упражнения 2. Убедитесь, что строки `debug_print` не появляются в отчёте. Затем удалите pragma и запустите снова -- убедитесь, что строки появляются как пропущенные.
# src/calculator.py (добавить к существующему файлу)
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ZeroDivisionError('cannot divide by zero')
return a / b
def absolute_value(x):
if x < 0:
return -x
return x
def debug_print(a, b): # добавьте pragma: no cover здесь
print(f'debug: a={a}, b={b}')
print(f'debug: sum={a + b}')
# Запустить: pytest --cov=src --cov-report=term-missing tests/
# С pragma: строки debug_print НЕ должны быть перечислены в Missing
# Без pragma: они появятся как непокрытые
Решение
# src/calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ZeroDivisionError('cannot divide by zero')
return a / b
def absolute_value(x):
if x < 0:
return -x
return x
def debug_print(a, b): # pragma: no cover
print(f'debug: a={a}, b={b}')
print(f'debug: sum={a + b}')
# Запустить: pytest --cov=src --cov-report=term-missing tests/
# С pragma: покрытие 100%, debug_print не в статистике
# Без pragma: покрытие падает и строки 17-18 появляются в Missing
No split tab
Настройки cookies
Мы используем необходимые cookies для работы сайта. С вашего разрешения мы также можем сохранять настройки сайта и использовать аналитические и рекламные cookies, чтобы понимать использование сайта и поддерживать развитие проекта.
* Вы всегда можете изменить свой выбор в настройках сайта.
Выберите категории cookies
Настройки аналитики
Можно отключить аналитику использования платформы. Также можно отправить в Google Analytics запрос на удаление данных об использовании этого сайта, связанных с этим браузером.
Учебный workspace
Учитесь, читая, запуская код и решая задачи.
Практикуйте программирование с пояснениями тем, упражнениями, инструментами browser IDE, проверкой regex и тренировкой печати кода в одном workspace.
Открывайте инструменты во вкладках.Упражнения, IDE-инструменты и тренажеры остаются доступными как вкладки сайта.
Переключайтесь без потери контекста.Переходите между пояснениями, кодом и утилитами, сохраняя свое место.
Используйте sidebar как карту.Левые панели содержат навигацию, настройки, файлы, libraries и управление инструментами.
PythonJavaScriptSQLite
Одна IDE, три практичных режима
Python в браузере.Запускайте небольшие скрипты, пробуйте библиотеки и тренируйте API-запросы без установки.
JavaScript для быстрых экспериментов.Проверяйте код для браузера и сравнивайте идеи рядом с учебными материалами.
SQLite для практики с данными.Открывайте обозреватель базы данных, изучайте таблицы, пишите запросы и учитесь SQL локально.
ТемаIDE
Работайте рядом в split tabs
Держите инструкции перед глазами.Откройте упражнение или справочную страницу рядом с IDE, вместо постоянных переключений.
Сравнивайте инструменты во время обучения.Размещайте проверки regex, пояснения и эксперименты с кодом рядом, когда это нужно для задачи.
Закройте split, когда закончите.Workspace вернётся к одной сфокусированной вкладке, а открытые вкладки сайта останутся доступны.
Тренажер слепой печати кода
Или просто текста
Тренажер рассчитан на физическую клавиатуру.Откройте этот раздел на ноутбуке или компьютере с широким экраном. На телефоне тренировка слепой печати не будет корректной.
Скорость: 0 зн/мин
0 слов/мин
Лучшая скорость (60с): 0 зн/мин
0 слов/мин
Ошибки: 0
Общее время: 0.0 с
Для активации режима слепого набора не подсматривайте на физическую клавиатуру.