0

Follows on from this answer about disabling autouse fixtures.

If I have this fixture

@pytest.fixture(autouse=True)
def mock_logger_error(request):
    with mock.patch('logging.Logger.error') as mock_error:
        yield

... how would I actually make use of mock_error (e.g. mock_error.assert_called_once_with(...)) in a test?

def test_something_possibly_emitting_error_message():
    ...
    if connection_ok:
        mock_error.assert_not_called()
    else:
        mock_error.assert_called_once_with(AnyStringWithRegex(r'ERROR\..*some error messsage.*'))

---> "Undefined variable: mock_error". I.e. although the patch is doing its job here, i.e. patching Logger.error, I don't know how to get a reference to it.

As can be seen here, it is possible to pass data from a fixture to a test, but that shows that you have to use return to do that. If you're using yield I don't think you can also use return...

mike rodent
  • 14,126
  • 11
  • 103
  • 157

2 Answers2

0

Ah, well I have found an answer, but it may not be ideal:

mock_error = None
@pytest.fixture(autouse=True)
def mock_logger_error(request):
    global mock_error
    with mock.patch('logging.Logger.error') as mock_error:
        yield

... mock_error works as intended in the test. This is a file-wide (although function-scope) fixture. May be more tricky in other cases. And if someone can come up with something more elegant that'd be nice.

mike rodent
  • 14,126
  • 11
  • 103
  • 157
0

You have several options:

First, rely on the fact that mocking replaces the mocked object, so you can change your test to be:

def test_something_possibly_emitting_error_message():
    import logging
    mock_error = logging.Logger.error
    ...
    if connection_ok:
        mock_error.assert_not_called()
    else:
        mock_error.assert_called_once_with(AnyStringWithRegex(r'ERROR\..*some error messsage.*'))

Note that it's important to do the import inside the test so it's done after the mock has taken place.

Another option is to return the mock from the fixture like so: yield mock_error. Then, change your test definition to include the fixture explicitly:

def test_something_possibly_emitting_error_message(mock_logger_error):
    ...
    if connection_ok:
        mock_logger_error.assert_not_called()
    else:
        mock_logger_error.assert_called_once_with(AnyStringWithRegex(r'ERROR\..*some error messsage.*'))
Peter K
  • 1,959
  • 1
  • 16
  • 22
  • Thanks. I tried your first solution but got "ModuleNotFoundError: No module named 'logging.Logger'". I tried other permutations but they all got MNFError. I found I had to go "import logging" and then "mock_error = logging.Logger.error". Of the two I prefer this one because it kind of defeats the object to have to include an autouse fixture as one of the test method's parameters (clutter). However, to have to include 2 more lines in each such test ... hmmm – mike rodent Mar 18 '23 at 08:05
  • Ah yes... or... you can just have the "import logging" at the top of the file, and then the line "mock_error = logging.Logger.error" in each test does the job. Probably a toss up between that and "global mock_error". – mike rodent Mar 18 '23 at 08:40
  • Fixed the import in my answer. Btw, if you want to verify an error was printed you can use the caplog or capsys fixtures and not have to do the mock fixture at all. See https://docs.pytest.org/en/7.1.x/how-to/logging.html#caplog-fixture and https://docs.pytest.org/en/7.1.x/how-to/capture-stdout-stderr.html#accessing-captured-output-from-a-test-function – Peter K Mar 18 '23 at 15:31
  • Thanks, yes I knew about caplog and capsys. However, I just checked the docs on caplog, and find that you can fine-tune it better than I remembered (specific level, specific logger). So thanks for that, caplog = much better here. But this question of mine is also about the principle of this technique. – mike rodent Mar 18 '23 at 18:07