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 к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
