Как проверять код на Python: основные методы и подходы

Раздел: Python -> Отладка и тестирование

Методы проверки кода Python

Проверка кода включает в себя не только тестирование, но и статический анализ, отладку и логирование. В этой статье рассматриваются наиболее популярные подходы с акцентом на практические примеры.

Какой инструмент обеспечивает максимальную гибкость и удобство при написании тестов?

Наиболее эффективным решением для тестирования Python-кода считается pytest. Он позволяет писать тесты в минималистичном стиле, используя обычные assert, поддерживает параметризацию, фикстуры и множество плагинов.

# Пример простого теста с pytest
# файл test_sample.py
def test_addition():
    assert 1 + 1 == 2

def test_subtraction():
    assert 3 - 2 == 1

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

Запуск тестов выполняется командой pytest test_sample.py в терминале. Результат - зеленая точка за каждый пройденный тест.

Типичные проблемы:

  • Непонимание механизма фикстур (scope, autouse). Решение: изучить документацию, начать с простых @pytest.fixture.
  • Сложность настройки плагинов (например, coverage). Решение: использовать pytest-cov с минимальной конфигурацией.
  • Ошибки импорта при запуске. Решение: запускать pytest из корня проекта или использовать PYTHONPATH.

Как провести тестирование без установки сторонних библиотек?

Модуль unittest является частью стандартной библиотеки Python. Он предоставляет класс TestCase, методы assertEqual, setUp, tearDown и позволяет создавать наборы тестов.

import unittest

class TestMath(unittest.TestCase):
    def test_add(self):
        self.assertEqual(1 + 1, 2)

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

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

Запуск: python test_unittest.py.

Распространенные ошибки:

  • Игнорирование if __name__ == '__main__'. Без него тесты не запускаются при импорте.
  • Путаница между setUp и setUpClass. Первый выполняется перед каждым тестом, второй - один раз для класса.
  • Забытие сброса состояния между тестами. Решение: использовать tearDown.

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

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

def factorial(n):
    """
    Вычисляет факториал n.
    >>> factorial(5)
    120
    >>> factorial(0)
    1
    """
    if n == 0:
        return 1
    return n * factorial(n-1)

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

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

Запуск: python doctest_example.py -v для подробного вывода.

Проблемы:

  • Чувствительность к пробелам в ожидаемом выводе. Решение: использовать # doctest: +NORMALIZE_WHITESPACE.
  • Не подходит для тестирования сложной логики с побочными эффектами.
  • Ошибки в docstring могут быть не замечены, если запускать без флага -v.

Как обнаружить ошибки типов до выполнения программы?

Статический анализатор типов mypy проверяет аннотации типов и выявляет несоответствия. Это помогает ловить баги на этапе написания кода.

# file: type_example.py
def greet(name: str) -> str:
    return 'Hello, ' + name

result = greet(42)  # Ошибка: int вместо str

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

Запуск: mypy type_example.py выдаст ошибку несовместимости типов.

Частые сложности:

  • Отсутствие аннотаций в сторонних библиотеках. Решение: установить types-* пакеты или использовать --ignore-missing-imports.
  • Излишняя строгость для динамического кода. Решение: настроить mypy.ini с параметром strict = false.
  • Путаница с типами Union, Optional. Рекомендуется изучить модуль typing.

Как пошагово выполнить код для поиска логических ошибок?

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

import pdb

def compute(x):
    y = x * 2
    pdb.set_trace()  # точка останова
    z = y + 10
    return z

result = compute(5)

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

Запуск: python pdb_example.py. В консоли pdb доступны команды: n (next), c (continue), p variable (печать).

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

  • Оставление pdb.set_trace() в продакшн-коде. Решение: использовать условную отладку или удалять перед коммитом.
  • Незнание команд pdb (например, l для листинга, q для выхода). Рекомендуется держать шпаргалку.
  • Проблемы с многопоточностью - pdb не работает надежно с потоками.

Как отлаживать программу с помощью логов, не засоряя код print?

Модуль logging предоставляет гибкую систему записи сообщений с разными уровнями (DEBUG, INFO, WARNING, ERROR). Логи можно выводить в консоль, файл, сеть.

import logging

logging.basicConfig(level=logging.DEBUG)

def divide(a, b):
    logging.debug(f'Деление {a} на {b}')
    if b == 0:
        logging.error('Попытка деления на ноль')
        return None
    return a / b

print(divide(10, 2))
print(divide(10, 0))

Результат: в консоль выводятся строки с уровнем DEBUG и ERROR.

Проблемы:

  • Конфигурация логгеров может быть запутанной (корневой логгер, форматтеры, хендлеры). Решение: использовать basicConfig для простых случаев, а для сложных - отдельный файл конфигурации.
  • Избыточность логов в продакшне. Решение: установить уровень WARNING или ERROR.
  • Неверное использование уровней (например, DEBUG для обычных сообщений). Рекомендуется придерживаться общепринятой семантики.

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

Ниже приведены подробные примеры с кодом и ожидаемым результатом для каждого из описанных методов.

1. Параметризованные тесты в pytest с фикстурами

Пример
import pytest

@pytest.fixture
def sample_data():
    return {'a': 10, 'b': 20}

@pytest.mark.parametrize('key, expected', [
    ('a', 10),
    ('b', 20),
    ('c', KeyError)
])
def test_fixture_values(sample_data, key, expected):
    if key == 'c':
        with pytest.raises(KeyError):
            _ = sample_data[key]
    else:
        assert sample_data[key] == expected
$ pytest test_param.py -v
============================= test session starts ==============================
collected 3 items

test_param.py::test_fixture_values[a-10] PASSED
test_param.py::test_fixture_values[b-20] PASSED
test_param.py::test_fixture_values[c-KeyError] PASSED

============================== 3 passed in 0.02s ===============================

2. Тестирование с unittest и mock

Пример
import unittest
from unittest.mock import patch
from math import sqrt

def fetch_value(calculator, x):
    return calculator.sqrt(x)

class TestWithMock(unittest.TestCase):
    @patch('math.sqrt')
    def test_fetch(self, mock_sqrt):
        mock_sqrt.return_value = 25
        result = fetch_value(sqrt, 100)  # передаем реальную функцию, но она заменена mock
        self.assertEqual(result, 25)
        mock_sqrt.assert_called_once_with(100)

if __name__ == '__main__':
    unittest.main()
$ python test_mock.py
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

3. Доктест с обработкой исключений

Пример
def safe_divide(a, b):
    """
    Делит a на b, возвращает None при ошибке.

    >>> safe_divide(10, 2)
    5.0
    >>> safe_divide(10, 0) is None
    True
    >>> safe_divide('a', 2)  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    TypeError: unsupported operand type(s) for /: 'str' and 'int'
    """
    try:
        return a / b
    except ZeroDivisionError:
        return None

if __name__ == '__main__':
    import doctest
    doctest.testmod(verbose=True)
$ python doctest_adv.py
Trying:
    safe_divide(10, 2)
Expecting:
    5.0
ok
Trying:
    safe_divide(10, 0) is None
Expecting:
    True
ok
Trying:
    safe_divide('a', 2)
Expecting:
    Traceback (most recent call last):
    TypeError: unsupported operand type(s) for /: 'str' and 'int'
ok
2 items had no tests:
    __main__
    __main__.safe_divide
1 items passed all tests:
   3 tests in __main__.safe_divide
3 tests in 3 items.
3 passed and 0 failed.
Test passed.

4. Статический анализ с mypy для класса

Пример
from typing import List, Optional

class Person:
    def __init__(self, name: str, age: Optional[int] = None) -> None:
        self.name = name
        self.age = age

    def greet(self) -> str:
        if self.age:
            return f'Привет, меня зовут {self.name}, мне {self.age} лет'
        return f'Привет, я {self.name}'

def introduce(people: List[Person]) -> None:
    for person in people:
        print(person.greet())

# Ошибка: передаётся число вместо списка
introduce(123)
$ mypy class_example.py
class_example.py:18: error: Argument 1 to "introduce" has incompatible type "int"; expected "List[Person]"
Found 1 error in 1 file (checked 1 source file)

5. Пошаговая отладка с pdb и условными брейкпоинтами

Пример
def complex_loop(n):
    total = 0
    for i in range(n):
        total += i
        if i == 3:
            import pdb
            pdb.set_trace()  # остановка только при i == 3
        total *= 1
    return total

complex_loop(10)
$ python pdb_conditional.py
> /Users/user/pdb_conditional.py(7)complex_loop()
-> total *= 1
(Pdb) p i
3
(Pdb) p total
6
(Pdb) c

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

Пример
import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG)

# Консольный вывод
console = logging.StreamHandler()
console.setLevel(logging.INFO)
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
console.setFormatter(formatter)
logger.addHandler(console)

# Файл с ротацией
file_handler = RotatingFileHandler('app.log', maxBytes=100, backupCount=2)
file_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)

logger.debug('Отладочное сообщение')
logger.info('Информация')
logger.warning('Предупреждение')
logger.error('Ошибка')
Консоль:
myapp - INFO - Информация
myapp - WARNING - Предупреждение
myapp - ERROR - Ошибка

Файл app.log:
2025-03-27 20:15:30,123 - myapp - DEBUG - Отладочное сообщение
2025-03-27 20:15:30,123 - myapp - INFO - Информация
2025-03-27 20:15:30,123 - myapp - WARNING - Предупреждение
2025-03-27 20:15:30,123 - myapp - ERROR - Ошибка

Проверка кода Python - comments

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