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

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

@potapov_me

Платформа

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

Контент

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

Компания

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

Аккаунт

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

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

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Отладка тестов: понимаем ошибки и быстро чиним
debugging_tests

Отладка тестов: понимаем ошибки и быстро чиним

Учимся читать traceback, использовать инструменты отладки, проверять coverage и закрывать типичные ошибки новичков за минуты

⚠️ Урок устарел. Свежая версия в курсе pytest-basics: Отладка: понимаем ошибки.

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

🕵️‍♂️ До: 30 минут поиска, паника, случайные исправления
⚡ После: 2 минуты анализа, уверенность, точные исправления

✅ Читаете traceback как профессионал — находите проблему за 30 секунд
✅ Используете правильные инструменты — pdb, логи, частичные проверки
✅ Знаете частые ошибки новичков — избегаете 80% проблем с самого начала

Реальные ошибки новичков

def test_add_task(): tasks = [] result = add_task(tasks, "buy milk") assert result == ["buy milk"] # ✅ Проходит # 🐛 Ошибка: забыли проверить исходный список assert tasks == ["buy milk"] # 💥 Падает! add_task возвращает НОВЫЙ список def test_user_age(): user = User(age=25) assert user.age == 25 # ✅ Проходит assert user.is_adult is True # 💥 Падает! is_adult вычисляется, а не хранится def test_api_response(): response = get("/users/1") assert response.status_code == 200 # ✅ Проходит assert response.json() == {"id": 1, "name": "Alice"} # 💥 Падает! Данные другие

Чтение traceback: от страха к пониманию

# ❌ Падает def test_calculate_total(): items = [{"price": 10}, {"price": 20}] > total = calculate_total(items) E TypeError: calculate_total() missing 1 required positional argument: 'tax_rate' tests/test_cart.py:45: TypeError # 🎯 Разбор: # 1) > строка падения (calculate_total(items)) # 2) E — тип/сообщение ошибки # 3) tests/test_cart.py:45 — где упало # 4) Причина: забыли передать tax_rate # ✅ Исправление: def test_calculate_total(): items = [{"price": 10}, {"price": 20}] total = calculate_total(items, tax_rate=0.1) assert total == 33.0

Современные инструменты отладки

Быстрая печать

Просто выводим входы/выходы, чтобы понять, что реально происходит, без захода в отладчик.

def test_complex_calculation(): data = load_test_data() print(f"📊 Input: {data}") result = complex_algorithm(data) print(f"🎯 Result: {result}") assert result == expected_value

pdb — встроенный пошаговый отладчик

Позволяет остановить выполнение, пройтись по коду, посмотреть переменные, не выходя из терминала.

def test_debug_with_pdb(): user = create_user("test") import pdb; pdb.set_trace() # 🛑 Остановка здесь # В консоли: # n/next — выполнить строку и перейти дальше # s/step — войти в функцию # p user — посмотреть переменную # l — показать контекст кода # c — продолжить до следующего брейкпоинта # q — выйти result = process_user(user) assert result.is_valid

Breakpoints в IDE (визуальный отладчик)

Ставим точку останова в редакторе и шагами смотрим переменные в UI — удобнее, чем pdb, если работаете в IDE.

  • VS Code: поставьте точку, «Debug Test», смотрите переменные.
  • PyCharm: breakpoint → Debug → Step Over/Into.

Логирование и частичные проверки (когда print/pdb мало)

Логи помогают понять последовательность событий, а частичные проверки (только важные поля) упрощают отладку ответов API.

def test_with_logging(caplog): with caplog.at_level("DEBUG"): result = complex_operation() assert "Starting operation" in caplog.text assert any("error" in rec.message for rec in caplog.records)
def test_api_response(): response = get("/users/1") assert response.status_code == 200 data = response.json() assert data["id"] == 1 assert "name" in data assert data["email"].endswith("@example.com")

📋 Таблица частых ошибок и решений

ОшибкаСимптомыРешение
ImportErrorcannot import name / ModuleNotFoundErrorPYTHONPATH=. pytest или pip install -e .
FixtureNotFoundfixture 'X' not foundПроверьте имя, conftest.py, перезапустите IDE
Float comparison0.1 + 0.2 != 0.3pytest.approx(0.3)
Async issuesзабыли await, тест завершается до выполнения корутиныдобавьте await и @pytest.mark.asyncio
Mutation bugsисходные данные меняютсяделайте .copy() и проверяйте оригинал
Flaky testsто проходит, то нетуберите общее состояние, изолируйте данные

Частые ошибки новичков и решения

# ❌ Float: прямое сравнение def test_float_math_bad(): assert 0.1 + 0.2 == 0.3 # 💥 Падает # ✅ Решение def test_float_math_good(): assert 0.1 + 0.2 == pytest.approx(0.3) # ❌ Async: забыли await def test_async_function_bad(): result = async_function() # 💥 Падает, нет await assert result == "expected" # ✅ Решение @pytest.mark.asyncio async def test_async_function_good(): result = await async_function() assert result == "expected" # ❌ Мутация исходных данных def test_data_processing_bad(): data = [1, 2, 3] process_data(data) assert data == [1, 2, 3] # 💥 Исходный список изменён # ✅ Решение def test_data_processing_good(): data = [1, 2, 3] original = data.copy() process_data(data) assert original == [1, 2, 3]

Практика: станьте детективом

Задача 1: найдите и исправьте ошибку

# Баговый код (для эксперимента) def calculate_discount(price: float, discount: float) -> float: return price * (1 - discount / 5) # 🐛 Ошибка в формуле def test_calculate_discount(): price = 100 discount = 20 print(f"💰 Price: {price}, Discount: {discount}") result = calculate_discount(price, discount) print(f"🎯 Result: {result}") # Подсказка: 96.0 вместо 80.0 — изучите формулу скидки assert result == 96.0 # после анализа

Задача 2: отладьте с pdb

# Баговый пайплайн def process_step_1(data): return data.get("items", []) # может вернуть пустой список def process_step_2(items): return [item * 2 for item in items] # падение, если items не числа def process_step_3(items): if not items: raise ValueError("No items to process") return {"status": "completed", "items": items} # Тест для отладки def test_complex_workflow(): data = load_test_data() print(f"📦 Loaded data: {data}") import pdb; pdb.set_trace() # p data, n/step через шаги, p result на каждом шаге result = process_step_1(data) result = process_step_2(result) result = process_step_3(result) assert result.status == "completed"

Решения (чтобы себя проверить)

# Исправление скидки def calculate_discount(price: float, discount: float) -> float: return price * (1 - discount / 100)
# Подсказка по pdb для workflow: # В pdb: p data, n (или s) пошагово, p result после каждого шага. # Баг: смешанные типы или пустые items в данных. # Починка: фильтруйте/валидируйте входные данные или обработайте пустой список перед step_3.

🚀 Шпаргалка: отладка за 60 секунд

  1. Прочитайте traceback — строка с > + сообщение.
  2. Добавьте печать/лог — посмотрите входы/выходы.
  3. Проверьте входные данные.
  4. Разбейте тест на шаги.
  5. Исправьте причину, не симптом.

Полезные флаги pytest для отладки

pytest --pdb # 🛑 PDB при падении pytest --lf # 🔄 Последние упавшие pytest -x # ⏹️ Стоп после первого падения pytest -v # 📊 Подробный вывод pytest -s # 🖨️ Показать print pytest --tb=short # 📄 Короткий traceback pytest --durations=10 # ⏱️ Топ-10 медленных тестов pytest test_file.py::test_name --pdb # 🎯 PDB только для одного теста

📊 Coverage: видим дыры в тестах

Проблема: Вы написали тесты, но не знаете, какие части кода они проверяют. Может быть критичная логика вообще не покрыта?

Решение: Coverage (покрытие кода тестами) — метрика, которая показывает, какие строки кода выполнялись во время тестов.

Установка и базовое использование

# Установка pip install pytest-cov # Запуск с coverage pytest --cov=src # С отчётом, какие строки не покрыты pytest --cov=src --cov-report=term-missing # HTML отчёт (красиво) pytest --cov=src --cov-report=html open htmlcov/index.html # Fail если coverage < 70% pytest --cov=src --cov-fail-under=70

Интерпретация метрик

---------- coverage: platform darwin, python 3.11.0 ----------- Name Stmts Miss Cover Missing ------------------------------------------------------ src/__init__.py 0 0 100% src/app.py 45 5 89% 23-27 src/cache.py 20 8 60% 15-22 src/utils.py 10 0 100% ------------------------------------------------------ TOTAL 75 13 83%

Что это значит:

  • Stmts — количество строк кода
  • Miss — строки, которые не выполнялись в тестах
  • Cover — процент покрытия
  • Missing — номера строк, которые не покрыты

Интеграция с IDE

VS Code:

  1. Установите расширение Coverage Gutters
  2. Запустите pytest --cov=src --cov-report=xml
  3. Нажмите Watch в статус-баре
  4. Зелёные линии = покрыты, красные = не покрыты

PyCharm:

  • Встроенная поддержка: Run with Coverage
  • Зелёные/красные полосы на полях редактора

Что считать хорошим покрытием?

# ✅ 70%+ coverage — индустриальный стандарт # ✅ 80%+ coverage — отличный результат # ⚠️ 100% coverage — может быть антипаттерном # Почему 100% — антипаттерн? def format_name(first, last): if not first or not last: raise ValueError("Names required") return f"{first} {last}".title() # ❌ Тест для 100% coverage (бесполезный) def test_format_name_100_percent(): assert format_name("john", "doe") == "John Doe" with pytest.raises(ValueError): format_name("", "") with pytest.raises(ValueError): format_name("john", "") # Погоня за 100% вместо проверки реальных сценариев # ✅ Тест для важной логики def test_format_name_business_logic(): assert format_name("john", "doe") == "John Doe" with pytest.raises(ValueError): format_name("", "doe")

Coverage в CI/CD

Добавьте в ваш CI пайплайн:

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

Практика: найдите непокрытый код

# Код с дырой в покрытии def process_payment(amount, card): if amount <= 0: raise ValueError("Invalid amount") if card.is_expired(): # 🚨 Эта ветка не покрыта raise ValueError("Card expired") return charge_card(card, amount) # Текущий тест def test_process_payment(): card = Card(number="1234", expires="12/25") result = process_payment(100, card) assert result.success is True # Запустите coverage и увидите, что строки с is_expired не покрыты # Добавьте тест для истёкшей карты def test_process_payment_expired_card(): card = Card(number="1234", expires="01/20") with pytest.raises(ValueError, match="expired"): process_payment(100, card)

Частые вопросы

Вопрос 1: Нужно ли покрывать тестами сеттеры/геттеры? Ответ: Нет. Фокусируйтесь на бизнес-логике, а не на тривиальном коде.

Вопрос 2: Как покрыть код, который зависит от внешних сервисов? Ответ: Используйте моки (урок "Мокинг"). Coverage считает выполненными замоканные пути.

Вопрос 3: Coverage 95%, но баги всё равно есть. Почему? Ответ: Coverage показывает, что код выполнялся, но не что он правильно работает. Важны качественные assert'ы.

Чеклист успешной отладки

  • Прочитал traceback — понимаю где и почему упало
  • Проверил входные/выходные данные (print/log)
  • Использовал подходящий инструмент (print/pdb/IDE)
  • Проверил частые ошибки (float, async, мутации, импорты)
  • Изолировал проблему — разбил сложный тест на шаги
  • Исправил причину, а не просто ожидание
  • Проверил coverage — нет дыр в критичной логике

Поздравляю! Теперь отладка — системный навык, а не паника. И вы видите, что реально покрыто тестами. 🔧

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

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

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

Далее: Мокинг: тестируем код с API без настоящего API