Testing lru_cache functions in Python with pytest

Rishabh
2 min readMar 23, 2019

--

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…

Debugging the test using pdb

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:

  1. https://docs.python.org/3/library/functools.html#functools.lru_cache
  2. https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch
  3. https://www.python.org/dev/peps/pep-0318/
  4. https://docs.pytest.org/latest/parametrize.html

Thanks to @codewithanthony for reviewing this post.

--

--

Rishabh

Software engineer specializing in accessibility. A11y advocate. ♥️s design, astrophysics, art, travel, photography & writing.