caplog в pytest: проверяем полезное сообщение об ошибке

Импорт пропустил строку с неизвестным кодом товара. Файл обработан, исключения нет, но объяснить пропуск пользователю нечем. Тест результата этого не заметил: нужна отдельная проверка сообщения, по которому разбирают отказ.

Выбрать информацию, которая нужна при разборе

В журнале должны остаться причина и идентификатор строки. Полное содержимое входа здесь не требуется: оно мешает найти ошибку и может содержать лишние сведения. В примере обработчик принимает уже выделенный код товара и номер строки; при неизвестном коде пишет warning и возвращает None. Тест закрепляет обе части договорённости.

Проверять весь форматированный текст обычно неудобно. В него попадают время, оформление обработчика и другие детали конфигурации. caplog.records даёт объекты LogRecord до форматирования. Через них можно сравнить уровень, имя логгера, вычисленное сообщение и дополнительные поля отдельно.

Ограничить захват нужным уровнем

caplog.at_level временно устанавливает уровень захвата. Указываем тот же логгер, которым пользуется функция. Локальный контекст возвращает настройку после блока, поэтому тест не меняет журналирование остальных сценариев. Сопоставление по имени полезно и при подключении библиотек: их предупреждения не должны случайно удовлетворять проверку нашего импорта.

Сохраните пример в test_logging.py. Поле row_id добавляется через extra; оно подходит для машинного поиска конкретной строки. Текст объясняет причину человеку. Раздельная проверка позволяет менять оформление журналов без переписывания теста поведения.

import logging
logger = logging.getLogger("catalog.import")

def resolve(code, row_id, known):
    if code not in known:
        logger.warning("unknown product: %s", code,
                       extra={"row_id": row_id})
        return None
    return known[code]

def test_unknown_product_is_explained(caplog):
    with caplog.at_level(logging.WARNING, logger="catalog.import"):
        result = resolve("missing", 17, {"A": 101})
    records = [r for r in caplog.records if r.name == "catalog.import"]
    assert result is None
    assert len(records) == 1
    record = records[0]
    assert record.levelno == logging.WARNING
    assert record.getMessage() == "unknown product: missing"
    assert record.row_id == 17

def test_known_product_has_no_warning(caplog):
    with caplog.at_level(logging.WARNING, logger="catalog.import"):
        assert resolve("A", 18, {"A": 101}) == 101
    assert not [r for r in caplog.records if r.name == "catalog.import"]

Отделить запись события от доставки

Пример проверяет создание записи в процессе Python. Если после этого сообщения теряются в файле или сборщике, потребуется проверять конфигурацию соответствующего обработчика. Сначала полезно локализовать проблему: приложение вообще сформировало событие или событие исчезло позже. caplog отвечает на первый вопрос и делает его воспроизводимым без внешнего сервиса.

Не привязывайте тест к общему числу всех записей процесса. Выберите записи конкретного логгера и утверждайте требуемое событие. Для успешной ветки отдельно убедитесь, что предупреждения нет: иначе случайный warning при каждом входе также мог бы пройти один положительный тест.

Добавить такую проверку к первым тестам

В открытой практике pytest начните с результата функции и граничного случая. Затем дополните собственный тест проверкой записи через caplog из статьи. Откройте программу курса: раздел конфигурации и логирования помогает связать сообщения приложения с его структурой.

Продолжить на практике

  • Python для продвинутых: ООП, типизация и тестирование ↗

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