pythonloggingpytestmonkeypatchingloguru

How to remove duplication of loguru logger patching in pytest


I am testing a function which calls loguru.logger.add("file.log") at the start. This causes issues during pytest execution. The file is written to a temp dir and thus is being used by another process (good ol' Windows) when clean-up happens.

PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'path/to/tmp_dir/file.log'

One solution is to patch loguru.logger.add on each test. But this results in much repeated (boilerplate?) code. For many tests, I don't need to refer to logger.add, just need patch it so the test runs.

@patch("loguru.logger.add")
def test_one(mock_logger_add):
    ...
    # Useful to check, but don't want to call this in EVERY test
    mock_logger_add.assert_called_once()

@patch("loguru.logger.add")
def test_two(mock_logger_add):
    ...
    # No need to check mock_logger_add, just want my code to run

How can I reduce this duplication?

Things I've tried:

e.g.

@pytest.fixture(autouse=True)
def patch_logger_add(monkeypatch):
    monkeypatch.setattr("loguru.logger.add", lambda *args, **kwargs: None)
    # or
    # monkeypatch.setattr("loguru.logger.add", MagicMock())

or

@pytest.fixture(autouse=True)
def no_logger_add(monkeypatch):
    monkeypatch.delattr("loguru.logger.add")

These don't work. Perhaps because, in order for loguru to work with pytest, we have to redefine caplog and that involves calling logger.add.

Note: I do not want to turn loguru off completely because I check the logs for errors in my tests.

Minimal reproducible example

compute_statistics.py

from loguru import logger

class ExampleClass:

    def compute(self):
        logger.add("0_example.log")
        logger.info("Starting to compute")
        logger.success("Finished computing")
        return "Done"

conftest.py

import pytest
from loguru import logger
from _pytest.logging import LogCaptureFixture

@pytest.fixture
def caplog(caplog: LogCaptureFixture):
    handler_id = logger.add(
        caplog.handler,
        format="{message}",
        level=0,
        filter=lambda record: record["level"].no >= caplog.handler.level,
        enqueue=False,  # Set to 'True' if your test is spawning child processes.
    )
    yield caplog
    logger.remove(handler_id)

test_compute_statistics.py

import logging
import pytest
from compute_statistics import ExampleClass
from unittest.mock import patch

@pytest.fixture(autouse=True)
def patch_logger_add():
    with patch("compute_statistics.logger.add"):
        yield

def assert_no_errors_logged(caplog):
    error_messages = [record for record in caplog.records if record.levelno >= logging.ERROR]
    num_errors = len(error_messages)
    assert num_errors == 0


def test_example_class(caplog):
    ex = ExampleClass()
    result = ex.compute()
    assert result == "Done"
    assert_no_errors_logged(caplog)

Solution

  • Since you say the patch decorator works but monkeypatch seemingly is not, I'm curious whether using patch as a context manager from within the fixture may solve your issue?

    Example

    @pytest.fixture(autouse=True)
    def patch_logger(caplog):
        with patch("your_module.logger.add"):
            yield