'python: clear / reset `@lru_cache` functool caching with every pytest testcase for undisturbed mocking
I combine mocking and caching in my code. The mocking
is (kind of) random for each pytest
as I do not know exactly, what will be returned in the real case. Hence I want to mock the very same function (in my example fct_child
) with different values in different testcases. The caching makes some problems however, as the return values (in my example of fct_parent
are cached and hence the mocked function is only reached in the first testcase and then always skipped due to the caching of the parent function. I need to find a way to clear/reset the cache between pytests.
In the following code the tests test_1
and test_2
can be executed independently of each over ($ pytest test_main.py::test_1
and $ pytest test_main.py::test_2
), successfully. If pytest runs over the full module ($ pytest test_main.py
), however, the second test crashes. Also the main part works ($ python test_main.py
), where I ensure, that caching works as expected.
So how can I fix the code, such that pytest
also passes when all testcases are executed (the $ pytest test_main.py
scenario)?
test_main.py
# test_main.py
from my_lib import fct_parent, get_n_calls_fct_child
class ChildMock:
def __init__(self, val_child):
self.n_calls_mock = 0
self.val_child = val_child
def fct(self):
self.n_calls_mock += 1
return self.val_child
def test_1(monkeypatch):
"""This test interacts with test_2:
Exectuing each test independently with pytest works, executing both in one run, fails.
This is due to the lru_cache being not cleaned.
"""
val_child = "mocked test 1"
child_mock = ChildMock(val_child)
with monkeypatch.context() as mpc:
mpc.setattr("my_lib.fct_child", child_mock.fct) # mocks fct_child to return ret_val
assert fct_parent() == val_child
assert fct_parent() == val_child
assert child_mock.n_calls_mock == 1
def test_2(monkeypatch):
"""This test interacts with test_1:
Exectuing each test independently with pytest works, executing both in one run, fails.
This is due to the lru_cache being not cleaned.
"""
val_child = "mocked test 2"
child_mock = ChildMock(val_child)
with monkeypatch.context() as mpc:
mpc.setattr("my_lib.fct_child", child_mock.fct) # mocks fct_child to return ret_val
assert fct_parent() == val_child
assert fct_parent() == val_child
assert child_mock.n_calls_mock == 1
if __name__ == "__main__":
assert fct_parent() == "unmocked"
assert fct_parent() == "unmocked"
n_calls_fct_child = get_n_calls_fct_child()
assert n_calls_fct_child == 1, f"{n_calls_fct_child=} should be == 1"
print("good: fct_child was only computed once")
my_lib.py
# my_lib.py
from functools import lru_cache
_n_child_calls = 0
@lru_cache(256)
def fct_parent():
return fct_child()
def fct_child():
global _n_child_calls
_n_child_calls += 1
return "unmocked"
def get_n_calls_fct_child():
return _n_child_calls
Solution 1:[1]
The following approach defines an @decorator
, which clear
s the cache
for the decorated function as soon as arriving in a new testcase
.
my_lib_fixed.py
import os
from functools import lru_cache, wraps
_pytest_cache_func = {} # Dict {'func.__name__: name_of_pytest_with_last_caching}
_n_child_calls = 0
def lru_cache_pytest_save(*lru_cache_args, **lru_cache_kwargs):
"""like @lru_cache, but additionally clears lru_cache of this function in between pytest testcases"""
# if you want to switch _pytest_save off:
# def decorator(func):
# return lru_cache(func)
# return decorator
def decorator(func):
func_cached = lru_cache(func)
@wraps(func)
def wrapper(*args, **kwargs):
pytest_current = os.environ.get("PYTEST_CURRENT_TEST")
if _pytest_cache_func.get(func_cached.__name__) != pytest_current:
func_cached.cache_clear()
_pytest_cache_func[func_cached.__name__] = pytest_current
return func_cached(*args, **kwargs)
return wrapper
return decorator
@lru_cache_pytest_save(256)
def fct_parent():
return fct_child()
def fct_child():
global _n_child_calls
_n_child_calls += 1
return "unmocked"
def get_n_calls_fct_child():
return _n_child_calls
def reset_n_calls_fct_child():
global _n_child_calls
_n_child_calls = 0
as the modulename is slightly different, you need minor modifications in
test_main_fixed.py
# test_main_fixed.py
from my_lib_fixed import fct_parent, get_n_calls_fct_child
class ChildMock:
def __init__(self, val_child):
self.n_calls_mock = 0
self.val_child = val_child
def fct(self):
self.n_calls_mock += 1
return self.val_child
def test_1(monkeypatch):
"""This test interacts with test_2:
Exectuing each test independently with pytest works, executing both in one run, fails.
This is due to the lru_cache being not cleaned.
"""
val_child = "mocked test 1"
child_mock = ChildMock(val_child)
with monkeypatch.context() as mpc:
mpc.setattr("my_lib_fixed.fct_child", child_mock.fct) # mocks fct_child to return ret_val
assert fct_parent() == val_child
assert fct_parent() == val_child
assert child_mock.n_calls_mock == 1
def test_2(monkeypatch):
"""This test interacts with test_1:
Exectuing each test independently with pytest works, executing both in one run, fails.
This is due to the lru_cache being not cleaned.
"""
val_child = "mocked test 2"
child_mock = ChildMock(val_child)
with monkeypatch.context() as mpc:
mpc.setattr("my_lib_fixed.fct_child", child_mock.fct) # mocks fct_child to return ret_val
assert fct_parent() == val_child
assert fct_parent() == val_child
assert child_mock.n_calls_mock == 1
if __name__ == "__main__":
assert fct_parent() == "unmocked"
assert fct_parent() == "unmocked"
n_calls_fct_child = get_n_calls_fct_child()
assert n_calls_fct_child == 1, f"{n_calls_fct_child=} should be == 1"
print("good: fct_child was only computed once")
now all of the 4 commands work:
$ python test_main.py
$ pytest test_main_fixed.py::test_1
$ pytest test_main_fixed.py::test_2
$ pytest test_main_fixed.py
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |