Elrond
Elrond

Reputation: 2112

Preventing logging file IO during test execution

I want to test a class that does logging when initialised and save logs to local file. Therefore, I'm mocking the logging piece of logic in order to avoid file IO when testing. This is pseudo-code representing how I've structured the tests

class TestClass:
    def test_1(self, monkeypatch):
        monkeypatch.setattr('dotted.path.to.logger', lambda *args: '')
        assert True

    def test_2(self, monkeypatch):
        monkeypatch.setattr('dotted.path.to.logger', lambda *args: '')
        assert True

    def test_3(self, monkeypatch):
        monkeypatch.setattr('dotted.path.to.logger', lambda *args: '')
        assert True

Note how monkeypatch.setattr() is copy-pasted across all methods. Considering that:

I think that monkey-patching should be abstracted at class level. How do we abstract monkeypatching at class level? I would expect the solution to be something similar to what follows:

import pytest
class TestClass:
    pytest.monkeypatch.setattr('dotted.path.to.logger', lambda *args: '')

    def test_1(self):
        assert True

    def test_2(self):
        assert True

    def test_3(self):
        assert True

This is where loggers are configured.

def initialise_logger(session_dir: str):
    """If missing, initialise folder "log" to store .log files. Verbosity:
    CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET."""
    os.makedirs(session_dir, exist_ok=True)
    logging.basicConfig(filename=os.path.join(session_dir, 'session.log'),
                        filemode='a',
                        level=logging.INFO,
                        datefmt='%Y-%m-%d %H:%M:%S',
                        format='|'.join(['(%(threadName)s)',
                                         '%(asctime)s.%(msecs)03d',
                                         '%(levelname)s',
                                         '%(filename)s:%(lineno)d',
                                         '%(message)s']))

    # Adopt NYSE time zone (aka EST aka UTC -0500 aka US/Eastern). Source:
    # https://stackoverflow.com/questions/32402502/how-to-change-the-time-zone-in-python-logging
    logging.Formatter.converter = lambda *args: get_now().timetuple()

    # Set verbosity in console. Verbosity above logging level is ignored.
    console = logging.StreamHandler()
    console.setLevel(logging.ERROR)
    console.setFormatter(logging.Formatter('|'.join(['(%(threadName)s)',
                                                     '%(asctime)s',
                                                     '%(levelname)s',
                                                     '%(filename)s:%(lineno)d',
                                                     '%(message)s'])))
    logger = logging.getLogger()
    logger.addHandler(console)


class TwsApp:
    def __init__(self):
        initialise_logger(<directory>)

Upvotes: 4

Views: 1032

Answers (2)

Elrond
Elrond

Reputation: 2112

In practice, I've put the fixture in /test/conftest.py. In fact, pytest automatically load fixture from files named conftest.py and can be applied in any module during the testing session.

from _pytest.monkeypatch import MonkeyPatch


@pytest.fixture(scope="class")
def suppress_logger(request):
    """Source: https://github.com/pytest-dev/pytest/issues/363"""
    # BEFORE running the test.
    monkeypatch = MonkeyPatch()
    # Provide dotted path to method or function to be mocked.
    monkeypatch.setattr('twsapp.client.initialise_logger', lambda x: None)
    # DURING the test.
    yield monkeypatch
    # AFTER running the test.
    monkeypatch.undo()



import pytest
@pytest.mark.usefixtures("suppress_logger")
class TestClass:
    def test_1(self):
        assert True

    def test_2(self):
        assert True

    def test_3(self):
        assert True

EDIT: I ended up using the following in conftest.py

@pytest.fixture(autouse=True)
def suppress_logger(mocker, request):
    if 'no_suppress_logging' not in request.keywords:
        # If not decorated with: @pytest.mark.no_suppress_logging_error
        mocker.patch('logging.error')
        mocker.patch('logging.warning')
        mocker.patch('logging.debug')
        mocker.patch('logging.info')

Upvotes: 0

wim
wim

Reputation: 362478

A cleaner implementation:

# conftest.py
import pytest

@pytest.fixture(autouse=True)
def dont_configure_logging(monkeypatch):
    monkeypatch.setattr('twsapp.client.initialise_logger', lambda x: None)

You don't need to mark individual tests with the fixture, nor inject it, this will be applied regardless.

Inject the caplog fixture if you need to assert on records logged. Note that you don't need to configure loggers in order to make logging assertions - the caplog fixture will inject the necessary handlers it needs in order to work correctly. If you want to customise the logging format used for tests, do that in pytest.ini or under a [tool:pytest] section of setup.cfg.

Upvotes: 1

Related Questions