Patch Stack
September 6, 2025 • 6 min read
ExitStack-powered teardown for reliable tests when cramming several cases
into one test method.
When you cram several cases into one test method (e.g., via a data provider), you get multiple problems:
- One flaky case can disable the whole method
- Failures are harder to pin down
- Mock setup is duplicated or tangled
You should treat each method as a separate test, but to fix this cleanly you should split cases into separate methods while keeping them clean by deduplicating the mocking.
The Solution: Context Manager for Mocking
To solve this, use a context manager for mocking. Create a small callable class that:
- Sets up all patches in
__enter__, stores theMagicMockas attributes - Accepts arguments to tweak behavior per test
- Tears everything down in
__exit__
from unittest import TestCase
from unittest.mock import patch
class MyMockContext:
def __init__(self, user_role="member", feature_on=False):
self.user_role = user_role
self.feature_on = feature_on
def __enter__(self):
self.p_role = patch("app.auth.get_role", return_value=self.user_role)
self.m_role = self.p_role.start()
self.p_feat = patch("app.features.is_enabled", return_value=self.feature_on)
self.m_feat = self.p_feat.start()
return self # expose mocks if you want to assert calls
def __exit__(self, exc_type, exc, tb):
self.p_role.stop()
self.p_feat.stop()
# Example function that uses the mocked dependencies
def do_the_thing():
from app.auth import get_role
from app.features import is_enabled
role = get_role()
if role == "admin" and is_enabled():
return "admin-feature-enabled"
elif role == "member":
return "ok-for-member"
else:
return "guest-access"
class MyServiceTests(TestCase):
def test_member_default(self):
with MyMockContext(user_role="member", feature_on=False):
result = do_the_thing()
self.assertEqual(result, "ok-for-member")
def test_admin_with_feature(self):
with MyMockContext(user_role="admin", feature_on=True):
result = do_the_thing()
self.assertEqual(result, "admin-feature-enabled")
def test_guest_user(self):
with MyMockContext(user_role="guest"):
result = do_the_thing()
self.assertEqual(result, "guest-access")
Benefits of This Approach
- Isolation by design: One case per method → TestX can quarantine just the flaky one. No data-provider "mega test" taking others down
- Teardown you can't forget: The context manager guarantees patches stop, preventing state leaks and flaky tests
- Refactor + Change friendly: When mocking strategy changes, you edit one place; all tests inherit it. Smaller diffs, fewer mistakes
Advanced Usage with ExitStack
For more complex scenarios with multiple patches, you can leverage ExitStack to manage them all:
from contextlib import ExitStack
from unittest.mock import patch
class AdvancedMockContext:
def __init__(self, **kwargs):
self.config = kwargs
self.mocks = {}
def __enter__(self):
self.stack = ExitStack()
# Dynamically create patches based on config
for key, value in self.config.items():
if key.startswith('mock_'):
target = key.replace('mock_', '').replace('_', '.')
patcher = patch(f"app.{target}", return_value=value)
mock = self.stack.enter_context(patcher)
self.mocks[key] = mock
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.stack.close()
class FlexibleServiceTests(TestCase):
def test_complex_scenario(self):
with AdvancedMockContext(
mock_auth_get_user={\"id\": 123, \"role\": \"admin\"},
mock_db_fetch_data=[{\"item\": 1}, {\"item\": 2}],
mock_cache_get=None
) as ctx:
result = complex_operation()
# Can access mocks via ctx.mocks if needed
ctx.mocks['mock_auth_get_user'].assert_called_once()
This pattern ensures your tests remain maintainable, isolated, and reliable while reducing boilerplate and preventing common mocking pitfalls.
Questions or feedback? Feel free to reach out!