Отладка и тестирование Python приложений

Раздел: Инструменты -> Отладка и тестирование

Основные подходы к проверке Python программ

Как автоматизировать проверку функций с помощью pytest?

Библиотека pytest предоставляет простой синтаксис для написания тестов, обширные возможности фикстур и параметризации. Это основной инструмент для модульного тестирования в современном Python.


# test_math.py
import pytest

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

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

# Запуск: pytest test_math.py -v

проверить python (проверка установки python)

Фикстуры позволяют подготовить данные:


import pytest

@pytest.fixture
def sample_data():
    return {'value': 10}

def test_fixture(sample_data):
    assert sample_data['value'] == 10

Python проверка (проверка кода python)

Параметризация для тестирования нескольких вариантов:


@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-5, 10, 5)
])
def test_param(a, b, expected):
    assert a + b == expected

тест кодов python (тестирование кода python)

Типичные ошибки:

  • Забыть установить pytest (pip install pytest). Возникает ModuleNotFoundError.
  • Не использовать префикс test_ для файлов и функций. pytest не найдёт тесты.
  • Фикстуры не возвращают данные, а только подготавливают окружение.

Как использовать встроенный модуль unittest для тестирования?

Модуль unittest входит в стандартную библиотеку и подходит для написания тестов в объектно-ориентированном стиле. Не требует дополнительных установок.


import unittest

def multiply(x, y):
    return x * y

class TestMathFunctions(unittest.TestCase):
    def test_multiplication(self):
        self.assertEqual(multiply(3, 4), 12)

if __name__ == '__main__':
    unittest.main()

проверить программу python (проверить программу на python)

Методы setUp и tearDown для подготовки и очистки:


class TestWithSetup(unittest.TestCase):
    def setUp(self):
        self.data = [1, 2, 3]

    def test_sum(self):
        self.assertEqual(sum(self.data), 6)

    def tearDown(self):
        del self.data

исправить ошибки в кодах python (исправление ошибок в коде python)

Частые проблемы:

  • Название метода теста не начинается с test. Метод не будет выполнен.
  • Забыть вызвать super().setUp() при наследовании.
  • Использовать assert вместо self.assert... сравнения.

Как пошагово отлаживать программу с pdb?

Отладчик pdb позволяет останавливать выполнение, просматривать переменные и выполнять код по шагам. Встраивается в программу одной строкой.


import pdb

def factorial(n):
    result = 1
    for i in range(1, n+1):
        pdb.set_trace()  # Вход в отладчик
        result *= i
    return result

print(factorial(3))

Команды внутри pdb: n (next), c (continue), p var (печать), l (список строк).

(Pdb) p i
1
(Pdb) p result
1
(Pdb) n
--Return--
> ...

Возможные сложности:

  • Множественные вызовы set_trace() в цикле приводят к частым остановкам.
  • Запуск программы в фоне делает pdb неприменимым.
  • Не установлен breakpoint (Python 3.7+ рекомендуется использовать встроенную функцию breakpoint()).
Как добавить диагностические сообщения для проверки состояния программы?

Модуль logging даёт гибкий механизм вывода сообщений разного уровня. Позволяет вести лог и не засорять код print.


import logging
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')

def divide(a, b):
    logging.debug(f"Вызов divide с a={a}, b={b}")
    if b == 0:
        logging.error("Деление на ноль!")
        return None
    return a / b

divide(10, 2)
divide(5, 0)
DEBUG - Вызов divide с a=10, b=2
DEBUG - Вызов divide с a=5, b=0
ERROR - Деление на ноль!

Настройка вывода в файл:


logging.basicConfig(filename='app.log', level=logging.INFO)

Ошибки при работе с logging:

  • Вызов basicConfig после первого логирования не применяется.
  • Забыть задать уровень, сообщения не выводятся.
  • Использовать print() вместо logging для отладки.
Как использовать assert для проверки условий?

Инструкция assert проверяет условие и выбрасывает AssertionError, если оно ложно. Подходит для быстрых проверок во время разработки.


def sqrt_approx(x):
    assert x >= 0, "Число должно быть неотрицательным"
    # реализация...
    return x ** 0.5

sqrt_approx(9)   # ОК
sqrt_approx(-1)  # AssertionError

Можно использовать двоеточие для сообщения об ошибке.

Предостережения:

  • assert отключается при запуске с флагом -O. Не подходит для проверок, которые обязательны в продакшене.
  • Сообщение в assert не должно содержать конфиденциальные данные.

Как проверить типы и найти потенциальные ошибки с помощью статического анализа?

Инструменты вроде mypy анализируют код до выполнения, выявляя несоответствия типов. Это помогает предотвратить баги на ранних этапах.


# mypy_example.py
def greet(name: str) -> str:
    return "Hello, " + name

greet(42)  # mypy укажет ошибку: Argument 1 has incompatible type 'int'
$ mypy mypy_example.py
mypy_example.py:4: error: Argument 1 to "greet" has incompatible type "int"; expected "str"

Аннотации типов не обязательны, но mypy использует их для проверки. Возможна интеграция с pytest через плагин pytest-mypy.

Трудности:

  • Сторонние библиотеки без аннотаций типов могут выдавать ложные срабатывания.
  • Строгая проверка требует времени на разметку кода.
  • Некоторые динамические возможности Python (например, type() для создания классов) не анализируются корректно.

Как встроить тесты в документацию функции с помощью доктестов?

Модуль doctest извлекает примеры из строк документации и выполняет их как тесты. Идеально для небольших проверок и документации.


def add(a, b):
    """
    Возвращает сумму двух чисел.

    >>> add(2, 3)
    5
    >>> add(-1, 1)
    0
    """
    return a + b

if __name__ == '__main__':
    import doctest
    doctest.testmod()
$ python mymodule.py -v
Trying:
    add(2, 3)
Expecting:
    5
ok
Trying:
    add(-1, 1)
Expecting:
    0
ok
1 items passed all tests.

Проблемы:

  • Строки вывода должны совпадать точно, включая пробелы.
  • При сложной логике доктесты становятся громоздкими.
  • Забыть импортировать doctest или добавить testmod.

Расширенные примеры проверки Python программ

Пример интеграционного теста с pytest и mock

Пример

# test_user.py
import pytest
from unittest.mock import Mock

def get_user(user_id):
    # имитация внешнего API
    response = requests.get(f"https://api.example.com/user/{user_id}")
    return response.json()

def test_get_user_success(monkeypatch):
    mock_response = Mock()
    mock_response.json.return_value = {'id': 1, 'name': 'Alice'}
    monkeypatch.setattr('requests.get', lambda url: mock_response)
    
    result = get_user(1)
    assert result == {'id': 1, 'name': 'Alice'}

Пример использует monkeypatch для замены вызова requests.get. Это позволяет тестировать код без реальных сетевых запросов.

$ pytest test_user.py -v
============================= test session starts ==============================
...
test_user.py::test_get_user_success PASSED

Отладка рекурсии с pdb и точками останова по условию

Пример

import pdb

def fibonacci(n):
    if n < 0:
        return None
    if n in (0, 1):
        return n
    pdb.set_trace() if n == 5 else None  # остановка только при n=5
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(6))
(Pdb) n
...

Можно устанавливать условные точки останова через pdb.set_trace() или breakpoint() с условием. Это ускоряет отладку больших вызовов.

Логирование в файл с ротацией и разными уровнями

Пример

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

handler = RotatingFileHandler('app.log', maxBytes=500, backupCount=3)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

def process_data(data):
    logger.debug("Начало обработки")
    if not data:
        logger.warning("Пустые данные")
        return
    try:
        result = 1 / data[0]
        logger.info("Результат получен: %s", result)
    except ZeroDivisionError:
        logger.error("Деление на ноль в данных")
    logger.debug("Конец обработки")

process_data([2])
process_data([0])

После выполнения в каталоге появится файл app.log и его ротационные копии. Ротация предотвращает бесконечное увеличение лога.

Проверка соответствия типов с mypy для сложного кода

Пример

# complex_types.py
from typing import List, Optional, Dict, Union

def parse_config(config: Dict[str, Union[str, int]]) -> Optional[str]:
    if 'name' in config and isinstance(config['name'], str):
        return config['name']
    return None

def run(items: List[int]) -> List[int]:
    return [x * 2 for x in items]

result = run([1,2,3])
print(result)
$ mypy complex_types.py --strict
Success: no issues found in 1 source file

При нарушении типов mypy укажет строку и суть ошибки. Рекомендуется включать флаг --strict для максимальной проверки.

Комбинированный подход: unittest + logging для отладки

Пример

import unittest
import logging

logging.basicConfig(level=logging.DEBUG)

def calculate_discount(price, percent):
    logging.debug("Расчёт скидки для цены %s и процента %s", price, percent)
    if price < 0 or percent < 0 or percent > 100:
        logging.error("Некорректные аргументы")
        raise ValueError
    discount = price * percent / 100
    return price - discount

class TestDiscount(unittest.TestCase):
    def test_discount_positive(self):
        self.assertEqual(calculate_discount(100, 10), 90)

    def test_discount_invalid_percent(self):
        with self.assertRaises(ValueError):
            calculate_discount(100, 110)

if __name__ == '__main__':
    unittest.main()

Логирование помогает увидеть внутреннее состояние при выполнении тестов, не останавливая программу. Все сообщения выводятся в консоль вместе с результатами тестов.

Доктесты со сложными структурами данных

Пример

def sort_dict_by_value(d):
    """
    Сортирует словарь по значениям и возвращает список кортежей.

    Пример:
    >>> sort_dict_by_value({'a': 3, 'b': 1, 'c': 2})
    [('b', 1), ('c', 2), ('a', 3)]

    >>> sort_dict_by_value({})
    []
    """
    return sorted(d.items(), key=lambda item: item[1])

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Вывод точно соответствует ожидаемому формату. Пробелы и порядок ключей имеют значение.

$ python doctest_example.py -v
...
1 items passed all tests.

проверить программу на Python - comments

En
проверить программу python (python)