## На яке питання відповідає coverage
У вас є тест-сьют -- але яку частину реального коду він виконує? Функція з трьома гілками може тестуватися лише на щасливому шляху, залишаючи дві гілки абсолютно не перевіреними. **Coverage** вимірює, які рядки (і які гілки) фактично виконалися під час запуску тестів.
Coverage -- це діагностичний інструмент: він показує, куди ваші тести не дивляться. Він не каже, чи є ваші тести *хорошими*.
## pytest-cov: coverage інтегрований у 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% покриття з тестами, що виявляють кожен реальний баг. Число -- це підказка, а не ціль.
Правильний спосіб використання coverage:
- **Знаходити прогалини** -- які нетривіальні шляхи коду не мають тестів? Чи вони важливі?
- **Встановлювати мінімальну планку** -- `--cov-fail-under=80` у CI запобігає випадковим регресіям (падіння покриття з 85% до 60% після рефакторингу -- тривожний сигнал)
- **Виявляти мертвий код** -- рядок, який *завжди* не покритий, може бути недосяжним і безпечним для видалення
Не женіться за числом. Женіться за змістовними тестами.
## Виключення коду з coverage
Не все варто вимірювати. Конфігураційні файли, заготовки міграцій, утиліти для налагодження -- виключіть їх:
**Через `.coveragerc`:**
```ini
[run]
omit =
tests/*
setup.py
*/migrations/*
[report]
exclude_lines =
if __name__ == .__main__.:
raise NotImplementedError
```
**Через inline-pragma:**
```python
if __name__ == '__main__': # pragma: no cover
main()
```
Коментар pragma виключає цей рядок з усіх звітів coverage. Використовуйте рідко -- лише для коду, що справді не може бути змістовно протестований.
## Код, що тестується
```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
```
## Запуск coverage і читання звіту
```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
def test_invalid_coupon():
with pytest.raises(ValueError, match='Unknown coupon: INVALID'):
calculate_price(100, is_member=False, coupon_code='INVALID')
```
Після додавання цих тестів coverage досягає 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))
```
Функція не з'явиться у статистиці coverage. Використовуйте рідко -- лише для коду, що справді не може бути юніт-протестований (утиліти налагодження, блоки `__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. Запустіть coverage знову і переконайтеся, що досягнуто 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__":`. Запустіть coverage і переконайтеся, що ці файли і рядки не з'являються у звіті.
# .coveragerc (створіть у кореневій директорії проекту)
[run]
omit =
# додайте: tests/*
# додайте: setup.py
[report]
exclude_lines =
# додайте шаблон для захисту __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 с
Щоб тренувати сліпий друк, не підглядайте на фізичну клавіатуру.