← Назад к вопросам

Измеряешь ли процент покрытия тестов

2.0 Middle🔥 61 комментариев
#Базы данных (SQL)#Безопасность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Покрытие тестами (Test Coverage)

Да, я активно измеряю и отслеживаю процент покрытия тестами. Это один из ключевых показателей качества кода и надежности приложения. Покрытие показывает, какой процент кода действительно выполняется во время тестирования.

Почему я измеряю покрытие

1. Гарантия качества

# Если код не покрыт тестами, его поведение неизвестно

def process_payment(amount, discount=0):
    total = amount - discount
    if total < 0:
        raise ValueError('Invalid total')
    charge_card(total)  # Когда это выполняется?
    return total

# Без тестов на path с ошибкой нельзя гарантировать,
# что charge_card() не вызовется при отрицательной сумме

2. Раннее обнаружение багов

# Низкое покрытие = непроверенный код
# Непроверенный код = потенциальные баги

def calculate_discount(customer_type, amount):
    if customer_type == 'premium':
        return amount * 0.2
    elif customer_type == 'regular':
        return amount * 0.1
    # Что если customer_type = 'unknown'?
    # Вернет None, затем может быть ошибка типа

# С тестом покрытия мы бы заметили этот edge case

3. Рефакторинг с уверенностью

# С хорошим покрытием можно безопасно менять код
# Если тесты пройдут, работа в порядке

def old_implementation(data):
    result = []
    for item in data:
        if item > 0:
            result.append(item * 2)
    return result

def new_implementation(data):
    return [item * 2 for item in data if item > 0]

# Если есть покрытие, мы знаем что both работают идентично

Инструменты для измерения покрытия

Coverage.py — стандартный инструмент

# Установка
pip install coverage

# Запуск тестов с измерением покрытия
coverage run -m pytest

# Вывод отчета
coverage report

# Детальный отчет в HTML
coverage html
open htmlcov/index.html

# Report по файлам
coverage report src/

Конфигурация .coveragerc

[run]
branch = True
source = src
omit = 
    */migrations/*
    */tests/*
    setup.py

[report]
precision = 2
show_missing = True
skip_covered = False
skip_empty = True

[html]
directory = htmlcov

Пример проекта с тестами

# src/calculator.py

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def divide(a, b):
    if b == 0:
        raise ValueError('Cannot divide by zero')
    return a / b
# tests/test_calculator.py

import pytest
from src.calculator import add, subtract, divide

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

def test_subtract():
    assert subtract(5, 3) == 2
    assert subtract(-1, -1) == 0

def test_divide():
    assert divide(10, 2) == 5
    assert divide(7, 2) == 3.5

def test_divide_by_zero():
    with pytest.raises(ValueError):
        divide(10, 0)
# Запуск с покрытием
$ coverage run -m pytest
$ coverage report

Name                      Stmts   Miss  Cover
-------------------------------------------
src/calculator.py            11      0   100%
tests/test_calculator.py     20      0   100%
-------------------------------------------
TOTAL                        31      0   100%

Минимальные требования к покрытию

В production коде

Основная логика: >= 90%
Вспомогательный код: >= 70%
Общий проект: >= 80%

Что обязательно должно быть покрыто

# ✅ Основные пути выполнения
def process_order(order):
    if order.total < 100:  # MUST TEST
        apply_discount(order)
    else:
        apply_premium_discount(order)
    
    return order

# ✅ Обработка ошибок
def fetch_user(user_id):
    if not user_id:
        raise ValueError('ID required')  # MUST TEST
    return get_user(user_id)

# ✅ Edge cases
def calculate_average(numbers):
    if not numbers:  # MUST TEST
        return 0
    return sum(numbers) / len(numbers)

Что НЕ нужно покрывать на 100%

# 1. Обработка очень редких исключений
try:
    operation()
except MemoryError:  # Редко тестируем
    log_critical_error()

# 2. Утилиты внутренних тестовых фреймворков
if __name__ == '__main__':  # Обычно не покрываем
    main()

# 3. Импорты и type hints
from typing import Optional  # Не покрываем
from dataclasses import dataclass  # Не покрываем

Типичные проблемы с покрытием

Проблема 1: Игнорирование низкого покрытия

# ❌ Плохо: не проверяем покрытие

def run_tests():
    pytest.main(['tests/'])
    # Не проверяем процент покрытия

# ✅ Хорошо: требуем определенное покрытие

def run_tests():
    result = pytest.main(['tests/', '--cov=src', '--cov-fail-under=80'])
    # Если покрытие < 80%, тест падает

Проблема 2: Фальшивые тесты (охватывают код, но не тестируют логику)

# ❌ Плохо: тест просто вызывает функцию

def test_process_order():
    result = process_order(create_test_order())
    # Не проверяем результат!
    # Покрытие есть, но логики нет

# ✅ Хорошо: тест проверяет результат

def test_process_order():
    order = create_test_order()
    result = process_order(order)
    assert result.total == expected_total
    assert result.status == 'processed'

Проблема 3: Избыточное тестирование

# ❌ Плохо: тестируем библиотечный код

def test_json_parsing():
    data = json.loads('{"key": "value"}')
    assert data['key'] == 'value'
    # Это тест json модуля, не нашего кода!

# ✅ Хорошо: тестируем нашу логику

def test_parse_user_data():
    user_json = '{"name": "Alice", "age": 30}'
    user = parse_user_data(user_json)
    assert user.name == 'Alice'
    assert user.age == 30

Интеграция покрытия в CI/CD

GitHub Actions

name: Tests and Coverage

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: 3.11
      
      - name: Install dependencies
        run: pip install -r requirements.txt
      
      - name: Run tests with coverage
        run: |
          coverage run -m pytest
          coverage report
          coverage html
      
      - name: Fail if coverage too low
        run: coverage report --fail-under=80
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v2

GitLab CI

test:
  image: python:3.11
  script:
    - pip install -r requirements.txt
    - coverage run -m pytest
    - coverage report --fail-under=80
  coverage: '/TOTAL.*\s+(\d+%)$/'

Мой подход к покрытию

1. Минимум 80% для основного кода

# В pyproject.toml
[tool.coverage.report]
fail_under = 80
show_missing = True

2. 100% для критичного кода

# src/payment/processor.py — критичный модуль
# MUST: 100% coverage
# Любая функция в платежах должна быть протестирована

3. Ветвовое покрытие (branch coverage)

# Обычное покрытие: строка выполнена или нет
# Ветвовое: все условия в if/else протестированы

coverage run --branch -m pytest
coverage report

4. Еженедельный мониторинг

# Смотрю тренд покрытия
coverage report > coverage_$(date +%Y%m%d).txt

# Если покрытие падает — ищу почему
# Новый код без тестов? Рефакторинг? Удаления?

Практический пример из реального проекта

# models/user.py

from datetime import datetime
from pydantic import BaseModel, EmailStr

class User(BaseModel):
    id: int
    email: EmailStr
    name: str
    age: int
    created_at: datetime = None
    
    def __init__(self, **data):
        super().__init__(**data)
        if self.created_at is None:
            self.created_at = datetime.now()
    
    def is_adult(self) -> bool:
        return self.age >= 18
    
    def formatted_email(self) -> str:
        return self.email.lower()
# tests/test_user.py

import pytest
from datetime import datetime
from models.user import User

def test_user_creation():
    user = User(
        id=1,
        email='alice@example.com',
        name='Alice',
        age=30
    )
    assert user.id == 1
    assert user.email == 'alice@example.com'
    assert user.created_at is not None

def test_user_is_adult():
    adult = User(id=1, email='alice@example.com', name='Alice', age=30)
    minor = User(id=2, email='bob@example.com', name='Bob', age=15)
    
    assert adult.is_adult() is True
    assert minor.is_adult() is False

def test_formatted_email():
    user = User(
        id=1,
        email='ALICE@EXAMPLE.COM',
        name='Alice',
        age=30
    )
    assert user.formatted_email() == 'alice@example.com'
$ coverage run -m pytest tests/test_user.py
$ coverage report models/user.py

Name               Stmts   Miss  Cover
--------------------------------------
models/user.py        15      0   100%

Итоговое резюме

Я активно использую покрытие тестами потому что:

  1. Гарантирует качество — непокрытый код = потенциальные баги
  2. Дает уверенность — изменяю код без страха
  3. Выявляет edge cases — низкое покрытие показывает непротестированные пути
  4. Автоматизируется — интегрируется в CI/CD
  5. Экономит время — лучше найти баг в тестах, чем в production

Мой стандарт:

  • Основной код: минимум 80%
  • Критичный код (платежи, auth): 100%
  • Ветвовое покрытие: включено
  • Требование к покрытию: в CI/CD
Измеряешь ли процент покрытия тестов | PrepBro