I’d like to share what I stumbled upon while writing a pytest unit test for a Python function which has functools
’s @lru_cache
decorator.
A simple spell
Let’s take an example of a fictional Python module, levitation.py
…
For demonstration purposes, let’s assume that the cast_spell
method is an expensive call and hence we have a need to decorate our levitate
function with an @lru_cache(maxsize=2)
decorator.
Writing a test
Now, let’s write a fictional unit test for our levitation
module with levitation_test.py
, where we assert that the cast_spell
function was invoked…
Running the test
At first glance, looking at the code nothing feels wrong here. But when run, this test fails with the following error…
AssertionError: Expected 'cast_spell' to be called once. Called 0 times.
… for the third parameterized run when ordinary_object='cauldron'
.
Debugging the test
Let’s debug the test with pdb
and use the cache_info
function, which is provided by lru_cache
, to investigate the function’s cache. Let’s set a breakpoint using import pdb; pdb.set_trace()
inside our test and run the test. Here’s a snippet of the test run…
As you can see, the CacheInfo
object’s hits
count increases each time the levitate
function is called.
For the first parameterized test run, when ordinary_object='quill'
, there is nothing in the cache. So the program counter steps inside the levitate
function and invokes our patched_cast_spell
.
For the second parameterized test run, ordinary_object='cushion'
, there is one item in the cache which is still less that the maxsize=2
defined in the lru_cache
decorator. So the program counter steps inside again.
Why does the test fail?
For the third parameterized test run, ordinary_object='cauldron'
, a cached value is returned because the maxsize
limit value is reached. Hence, the program counter doesn’t step inside the levitate
function and invoke our patched_cast_spell
.
Solution
The solution is to invoke the cache_clear
function, which is provided by lru_cache
, to clear/invalidate the function’s cache. Here’s the corrected test, where we use levitate.cache_clear()
before calling the levitate
function…
Further reading
I’ll leave you with some links for further reading:
Thanks to @codewithanthony for reviewing this post.