Alex Lin Wang 王帅

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 the MagicMock as 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!