{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreiccer3yxqujymsduesdpdcxkl3zc2nfrvxz7ba3m2tvljv4ukolki",
    "uri": "at://did:plc:tntgihx6r2e2gqexsx72grd5/app.bsky.feed.post/3mdbsfqauqld2"
  },
  "path": "/blog/202601/testing_exceptions_and_caches",
  "publishedAt": "2026-06-11T04:22:24.068Z",
  "site": "https://nedbatchelder.com",
  "tags": [
    "parameterizing exception\ntesting",
    "pytest docs",
    "StackOverflow answer",
    "pytest.raises",
    "@functools.lru_cache",
    "pytest-antilru",
    "@pytest.mark.parametrize",
    "@cache",
    "@lru_cache"
  ],
  "textContent": "Two testing-related things I found recently.\n\n# Unified exception testing\n\nKacper Borucki blogged about parameterizing exception\ntesting, and linked to pytest docs and a StackOverflow answer with similar approaches.\n\nThe common way to test exceptions is to use pytest.raises as a context manager, and have separate tests for the cases that succeed and those that fail. Instead, this approach lets you unify them.\n\nI tweaked it to this, which I think reads nicely:\n\n>\n>     from contextlib import nullcontext as produces\n>\n>     >\n>\n>     > import pytest\n>\n>     > from pytest import raises\n>\n>     >\n>\n>     > @pytest.mark.parametrize(\n>\n>     >     \"example_input, result\",\n>\n>     >     [\n>\n>     >         (3, produces(2)),\n>\n>     >         (2, produces(3)),\n>\n>     >         (1, produces(6)),\n>\n>     >         (0, raises(ZeroDivisionError)),\n>\n>     >         (\"Hello\", raises(TypeError)),\n>\n>     >     ],\n>\n>     > )\n>\n>     > def test_division(example_input, result):\n>\n>     >     with result as e:\n>\n>     >         assert (6 / example_input) == e\n>\n>     >\n\nOne parameterized test that covers both good and bad outcomes. Nice.\n\n# AntiLRU\n\nThe @functools.lru_cache decorator (and its convenience cousin `@cache`) are good ways to save the result of a function so that you don’t have to compute it repeatedly. But, they hide an implicit global in your program: the dictionary of cached results.\n\nThis can interfere with testing. Your tests should all be isolated from each other. You don’t want a side effect of one test to affect the outcome of another test. The hidden global dictionary will do just that. The first test calls the cached function, then the second test gets the cached value, not a newly computed one.\n\nIdeally, lru_cache would only be used on _pure_ functions: the result only depends on the arguments. If it’s only used for pure functions, then you don’t need to worry about interactions between tests because the answer will be the same for the second test anyway.\n\nBut lru_cache is used on functions that pull information from the environment, perhaps from a network API call. The tests might mock out the API to check the behavior under different API circumstances. Here’s where the interference is a real problem.\n\nThe lru_cache decorator makes a `.clear_cache` method available on each decorated function. I had some code that explicitly called that method on the cached functions. But then I added a new cached function, forgot to update the conftest.py code that cleared the caches, and my tests were failing.\n\nA more convenient approach is provided by pytest-antilru: it’s a pytest plugin that monkeypatches `@lru_cache` to track all of the cached functions, and clears them all between tests. The caches are still in effect during each test, but can’t interfere between them.\n\nIt works great. I was able to get rid of all of the manually maintained cache clearing in my conftest.py.",
  "title": "Testing: exceptions and caches",
  "updatedAt": "2026-01-25T20:32:21.000Z"
}