Suppose I have a decorator that collects all the functions it decorates to be called at some point in the future.
mydecorator.py
class CallLater(object):
funcs = []
def __init__(self, func):
self.funcs.append(func)
@classmethod
def call_now(cls, *args, **kwargs):
for func in cls.funcs:
func(*args, **kwargs)
Then, I have a function in a module, one of which will be saved by my decorator.
mymodule.py
import logging
from mydecorator import CallLater
logging.basicConfig(level=logging.INFO)
@CallLater
def log_a():
logging.info("A")
@CallLater
def log_b():
logging.info("B")
def log_c():
logging.info("C")
Now, if I import mymodule
and call CallLater.call_now()
, log_a
and log_b
will be called. But let's say that during testing, I want log_b
to be substituted with log_c
. I'll try to do the replacement with a mock.
mock_test.py
import logging
import pytest
from mymodule import log_a, log_c
from mydecorator import CallLater
logging.basicConfig(level=logging.INFO)
pytest_plugins = ('pytest_mock',)
def test_mocking(mocker, caplog):
mocker.patch('mymodule.log_b', log_c)
CallLater.call_now()
logs = [rec.message for rec in caplog.records]
assert logs == ["A", "C"]
But when I run pytest
, I see that my mock didn't work.
FAILED mock_test.py::test_mocking - AssertionError: assert ['A', 'B'] == ['A', 'C']
I imagine that 'mymodule.log_b'
is the wrong mocking target since it's not being invoked as mymodule.log_b()
, but I'm not sure what to use instead in this situation. Any advice is appreciated!
This is not possible as such, the problem being that the functions are already assigned to the list at load time. The only way I can see to patch this would be to patch CallLater.funcs
directly, which is a bit awkward, because you have to replace log_b
by log_c
manually - but here it goes:
import logging
from unittest import mock
from mymodule import log_c
from mydecorator import CallLater
logging.basicConfig(level=logging.INFO)
def test_mocking(caplog):
funcs = [log_c if f.__name__ == 'log_b' else f for f in CallLater.funcs]
with mock.patch.object(CallLater, 'funcs', funcs):
CallLater.call_now()
logs = [rec.message for rec in caplog.records]
assert logs == ["A", "C"]
Note that you cannot compare directly to the function (e.g. f == log_b
), because log_b
is the decorated function, not the function as saved in CallLater.funcs
.