[{"data":1,"prerenderedAt":1512},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002F":3},{"id":4,"title":5,"body":6,"description":193,"extension":1506,"meta":1507,"navigation":227,"path":1508,"seo":1509,"stem":1510,"__hash__":1511},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Findex.md","Patching Strategies for Complex Codebases",{"type":7,"value":8,"toc":1495},"minimark",[9,13,18,22,31,34,38,57,91,109,125,136,140,154,167,184,187,347,354,358,380,399,421,587,605,609,627,653,689,843,856,860,870,893,903,1066,1081,1085,1092,1107,1114,1139,1290,1296,1300,1394,1398,1412,1436,1462,1491],[10,11,5],"h1",{"id":12},"patching-strategies-for-complex-codebases",[14,15,17],"h2",{"id":16},"introduction-the-patching-paradox-in-large-systems","Introduction: The Patching Paradox in Large Systems",[19,20,21],"p",{},"In enterprise-grade Python applications, the pursuit of deterministic test isolation frequently collides with architectural reality. Monolithic service boundaries, deeply nested plugin registries, and legacy modules with implicit global state create a testing landscape where pure dependency injection is often impractical or economically unfeasible. This tension defines the patching paradox: mocks provide surgical isolation but introduce coupling to implementation details, while over-reliance on patching obscures architectural debt and inflates CI execution times.",[19,23,24,25,30],{},"The foundational principle for navigating this paradox is recognizing that patching is a tactical isolation mechanism, not a strategic architectural substitute. When applied indiscriminately, patches become fragile assertions that silently pass while production logic diverges. Conversely, when deployed surgically at well-defined boundaries, patches enable rapid feedback loops without requiring wholesale refactoring. The ",[26,27,29],"a",{"href":28},"\u002Fadvanced-mocking-test-doubles-in-python\u002F","Advanced Mocking & Test Doubles in Python"," framework establishes the decision matrix for when patching is necessary versus when interface extraction or dependency inversion should take precedence.",[19,32,33],{},"In large-scale systems, patching strategies directly impact CI pipeline velocity. Each patched reference incurs runtime overhead during test collection and execution. Unscoped patches leak state across test workers, triggering flaky failures that degrade developer trust. Enterprise codebases demand a disciplined approach: trace import paths rigorously, enforce strict contract validation, isolate interpreter-level mutations, and profile patch overhead before scaling test matrices. This article bridges foundational mock theory to production-grade patching architectures that handle circular dependencies, global state, async boundaries, and third-party integrations without compromising test determinism or CI throughput.",[14,35,37],{"id":36},"core-mechanics-import-paths-and-namespace-resolution","Core Mechanics: Import Paths and Namespace Resolution",[19,39,40,41,45,46,49,50,53,54,56],{},"Python's dynamic import system is the primary source of patching failures in complex projects. The ",[42,43,44],"code",{},"unittest.mock.patch"," function operates by temporarily replacing an object in a specific namespace. The critical distinction lies in understanding ",[42,47,48],{},"where"," (the namespace to mutate) versus ",[42,51,52],{},"what"," (the object being replaced). Misidentifying the ",[42,55,48],{}," parameter results in silent test passes, as the production code continues to reference the original, unpatched binding.",[19,58,59,60,63,64,67,68,71,72,75,76,79,80,83,84,87,88,90],{},"When a module executes ",[42,61,62],{},"from external_lib import Client",", Python binds ",[42,65,66],{},"Client"," directly into the local module's ",[42,69,70],{},"__dict__",". Patching ",[42,73,74],{},"external_lib.Client"," will have zero effect on the consuming module, which holds a direct reference to the original class. The correct target is ",[42,77,78],{},"my_app.services.external_lib.Client",". Conversely, ",[42,81,82],{},"import external_lib"," followed by ",[42,85,86],{},"external_lib.Client()"," resolves the attribute at call time. Patching ",[42,89,74],{}," works here because the lookup occurs dynamically against the module object.",[19,92,93,94,97,98,100,101,104,105,108],{},"Python's ",[42,95,96],{},"sys.modules"," cache further complicates resolution. Once a module is imported, it resides in ",[42,99,96],{}," as a singleton. Subsequent imports return the cached object, meaning patches applied after initial import may miss early initialization side effects. For comprehensive coverage, patches must be applied before the target module executes its top-level code, typically via pytest fixtures scoped to ",[42,102,103],{},"function"," or ",[42,106,107],{},"module"," level.",[19,110,111,112,116,117,120,121,124],{},"The ",[26,113,115],{"href":114},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002F","Deep Dive into unittest.mock"," documentation details the underlying context manager mechanics that guarantee restoration. Understanding these mechanics prevents cross-module leakage. Decorator-based patching (",[42,118,119],{},"@patch(...)",") applies patches before test execution and restores them immediately after, but it obscures the exact restoration boundary. Context manager usage (",[42,122,123],{},"with patch(...):",") provides explicit control, crucial when patching multiple interdependent references or when combining patches with parametrized fixtures.",[19,126,127,128,131,132,135],{},"To avoid namespace collisions, always trace the exact import path in the consuming module. Use ",[42,129,130],{},"inspect.getmodule()"," during debugging to verify which module object holds the reference. When patching across package boundaries, prefer fully qualified strings that match the runtime ",[42,133,134],{},"__name__"," attribute. This discipline eliminates the most common source of false-positive tests in large repositories.",[14,137,139],{"id":138},"strict-contract-enforcement-with-autospec","Strict Contract Enforcement with Autospec",[19,141,142,143,146,147,104,150,153],{},"Loose mocks accelerate test authoring but introduce severe maintenance debt. A standard ",[42,144,145],{},"MagicMock"," accepts any attribute access and method call, returning new mock objects recursively. This flexibility masks signature drift, deprecated parameters, and incorrect argument ordering. In evolving APIs, tests relying on loose mocks frequently pass while production code fails with ",[42,148,149],{},"TypeError",[42,151,152],{},"AttributeError",".",[19,155,156,159,160,163,164,166],{},[42,157,158],{},"autospec=True"," mitigates this by introspecting the target object's signature and restricting mock behavior to match the original interface. When a patched function is called with invalid arguments, ",[42,161,162],{},"autospec"," raises a ",[42,165,149],{}," identical to the real implementation. Attribute access is restricted to existing names on the spec object, preventing silent typos. This strictness transforms mocks from passive placeholders into active contract validators.",[19,168,169,170,172,173,175,176,179,180,183],{},"However, ",[42,171,162],{}," incurs measurable overhead. Recursive introspection on deeply nested object graphs increases test collection time and memory allocation. For performance-critical suites, limit ",[42,174,162],{}," to public API boundaries and use ",[42,177,178],{},"spec_set=True"," for known interfaces where you want to prevent attribute creation but avoid full signature validation. Avoid ",[42,181,182],{},"create=True"," in production tests; it bypasses spec validation entirely and reintroduces the brittleness autospec solves.",[19,185,186],{},"Combining autospec with property-based testing yields robust contract verification. By generating edge-case inputs with Hypothesis and asserting that mocked calls adhere to expected signatures, you catch boundary violations before they reach staging.",[188,189,194],"pre",{"className":190,"code":191,"language":192,"meta":193,"style":193},"language-python shiki shiki-themes github-light github-dark","import pytest\nfrom unittest.mock import patch, create_autospec\nfrom hypothesis import given, strategies as st\nfrom my_app.services import PaymentProcessor\n\nclass TestStrictContractEnforcement:\n @pytest.fixture\n def strict_processor(self):\n # Autospec validates signature and restricts attribute access\n return create_autospec(PaymentProcessor, instance=True, spec_set=True)\n\n @given(\n amount=st.floats(min_value=0.01, max_value=10000.0, allow_nan=False),\n currency=st.sampled_from([\"USD\", \"EUR\", \"GBP\"])\n )\n def test_signature_validation_with_hypothesis(self, strict_processor, amount, currency):\n # Hypothesis generates diverse inputs; autospec ensures calls match real signature\n strict_processor.charge(amount=amount, currency=currency)\n \n # Verify call structure matches expected contract\n strict_processor.charge.assert_called_once_with(amount=amount, currency=currency)\n \n # Attempting an invalid signature would raise TypeError immediately\n with pytest.raises(TypeError):\n strict_processor.charge(invalid_kwarg=True)\n","python","",[42,195,196,204,210,216,222,229,235,241,247,253,259,264,270,276,282,288,294,300,306,312,318,324,329,335,341],{"__ignoreMap":193},[197,198,201],"span",{"class":199,"line":200},"line",1,[197,202,203],{},"import pytest\n",[197,205,207],{"class":199,"line":206},2,[197,208,209],{},"from unittest.mock import patch, create_autospec\n",[197,211,213],{"class":199,"line":212},3,[197,214,215],{},"from hypothesis import given, strategies as st\n",[197,217,219],{"class":199,"line":218},4,[197,220,221],{},"from my_app.services import PaymentProcessor\n",[197,223,225],{"class":199,"line":224},5,[197,226,228],{"emptyLinePlaceholder":227},true,"\n",[197,230,232],{"class":199,"line":231},6,[197,233,234],{},"class TestStrictContractEnforcement:\n",[197,236,238],{"class":199,"line":237},7,[197,239,240],{}," @pytest.fixture\n",[197,242,244],{"class":199,"line":243},8,[197,245,246],{}," def strict_processor(self):\n",[197,248,250],{"class":199,"line":249},9,[197,251,252],{}," # Autospec validates signature and restricts attribute access\n",[197,254,256],{"class":199,"line":255},10,[197,257,258],{}," return create_autospec(PaymentProcessor, instance=True, spec_set=True)\n",[197,260,262],{"class":199,"line":261},11,[197,263,228],{"emptyLinePlaceholder":227},[197,265,267],{"class":199,"line":266},12,[197,268,269],{}," @given(\n",[197,271,273],{"class":199,"line":272},13,[197,274,275],{}," amount=st.floats(min_value=0.01, max_value=10000.0, allow_nan=False),\n",[197,277,279],{"class":199,"line":278},14,[197,280,281],{}," currency=st.sampled_from([\"USD\", \"EUR\", \"GBP\"])\n",[197,283,285],{"class":199,"line":284},15,[197,286,287],{}," )\n",[197,289,291],{"class":199,"line":290},16,[197,292,293],{}," def test_signature_validation_with_hypothesis(self, strict_processor, amount, currency):\n",[197,295,297],{"class":199,"line":296},17,[197,298,299],{}," # Hypothesis generates diverse inputs; autospec ensures calls match real signature\n",[197,301,303],{"class":199,"line":302},18,[197,304,305],{}," strict_processor.charge(amount=amount, currency=currency)\n",[197,307,309],{"class":199,"line":308},19,[197,310,311],{}," \n",[197,313,315],{"class":199,"line":314},20,[197,316,317],{}," # Verify call structure matches expected contract\n",[197,319,321],{"class":199,"line":320},21,[197,322,323],{}," strict_processor.charge.assert_called_once_with(amount=amount, currency=currency)\n",[197,325,327],{"class":199,"line":326},22,[197,328,311],{},[197,330,332],{"class":199,"line":331},23,[197,333,334],{}," # Attempting an invalid signature would raise TypeError immediately\n",[197,336,338],{"class":199,"line":337},24,[197,339,340],{}," with pytest.raises(TypeError):\n",[197,342,344],{"class":199,"line":343},25,[197,345,346],{}," strict_processor.charge(invalid_kwarg=True)\n",[19,348,111,349,353],{},[26,350,352],{"href":351},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002F","Autospec & Strict Mocking"," guide provides deeper analysis of performance trade-offs and recursive spec caching strategies. Enforcing strict contracts at the patch boundary reduces refactoring friction and ensures test suites evolve alongside production code.",[14,355,357],{"id":356},"global-state-and-interpreter-level-patching","Global State and Interpreter-Level Patching",[19,359,360,361,364,365,368,369,372,373,376,377,379],{},"Patching builtins, environment variables, and ",[42,362,363],{},"sys"," modules represents the highest-risk category in test isolation. These mutations affect the entire interpreter process, making them inherently incompatible with parallel execution unless carefully scoped. The ",[42,366,367],{},"__builtins__"," dictionary differs from the ",[42,370,371],{},"builtins"," module: the former is an implementation detail of the CPython interpreter, while the latter is the official public API. Patching ",[42,374,375],{},"builtins.open"," is safe and recommended; mutating ",[42,378,367],{}," directly risks interpreter instability.",[19,381,382,384,385,388,389,391,392,395,396,398],{},[42,383,96],{}," and ",[42,386,387],{},"sys.path"," mutations require deterministic restoration guarantees. When a test temporarily replaces a module in ",[42,390,96],{},", failure to restore the original reference on teardown causes subsequent tests to import the mocked version, triggering cascading failures. Thread-safe patching is equally critical: ",[42,393,394],{},"pytest-xdist"," spawns multiple worker processes, each with isolated ",[42,397,96],{}," caches, but shared state within a single worker can still cause race conditions if patches are applied asynchronously.",[19,400,401,402,405,406,409,410,412,413,416,417,420],{},"A robust pattern for interpreter-level patching uses context managers with explicit ",[42,403,404],{},"try\u002Ffinally"," blocks or pytest fixtures with ",[42,407,408],{},"yield",". This guarantees restoration regardless of assertion failures or unhandled exceptions. For ",[42,411,387],{}," modifications, prefer ",[42,414,415],{},"sys.path.insert(0, ...)"," with immediate cleanup over ",[42,418,419],{},"sys.path = [...]"," assignments, which break import resolution for standard library modules.",[188,422,424],{"className":190,"code":423,"language":192,"meta":193,"style":193},"import sys\nimport contextlib\nimport importlib\nimport pytest\n\n@contextlib.contextmanager\ndef patch_sys_module(target_module_name: str, replacement_module):\n \"\"\"Thread-safe sys.modules swap with deterministic restoration.\"\"\"\n original = sys.modules.get(target_module_name)\n sys.modules[target_module_name] = replacement_module\n try:\n yield replacement_module\n finally:\n if original is None:\n sys.modules.pop(target_module_name, None)\n else:\n sys.modules[target_module_name] = original\n\nclass TestInterpreterIsolation:\n def test_temporary_module_swap(self):\n class FakeLogger:\n @staticmethod\n def info(msg): pass\n\n with patch_sys_module(\"logging\", FakeLogger):\n import logging\n assert logging.info.__name__ == \"info\"\n # Production code using logging will now hit FakeLogger\n \n # Restoration verified\n import logging\n assert hasattr(logging, \"getLogger\")\n",[42,425,426,431,436,441,445,449,454,459,464,469,474,479,484,489,494,499,504,509,513,518,523,528,533,538,542,547,553,559,565,570,576,581],{"__ignoreMap":193},[197,427,428],{"class":199,"line":200},[197,429,430],{},"import sys\n",[197,432,433],{"class":199,"line":206},[197,434,435],{},"import contextlib\n",[197,437,438],{"class":199,"line":212},[197,439,440],{},"import importlib\n",[197,442,443],{"class":199,"line":218},[197,444,203],{},[197,446,447],{"class":199,"line":224},[197,448,228],{"emptyLinePlaceholder":227},[197,450,451],{"class":199,"line":231},[197,452,453],{},"@contextlib.contextmanager\n",[197,455,456],{"class":199,"line":237},[197,457,458],{},"def patch_sys_module(target_module_name: str, replacement_module):\n",[197,460,461],{"class":199,"line":243},[197,462,463],{}," \"\"\"Thread-safe sys.modules swap with deterministic restoration.\"\"\"\n",[197,465,466],{"class":199,"line":249},[197,467,468],{}," original = sys.modules.get(target_module_name)\n",[197,470,471],{"class":199,"line":255},[197,472,473],{}," sys.modules[target_module_name] = replacement_module\n",[197,475,476],{"class":199,"line":261},[197,477,478],{}," try:\n",[197,480,481],{"class":199,"line":266},[197,482,483],{}," yield replacement_module\n",[197,485,486],{"class":199,"line":272},[197,487,488],{}," finally:\n",[197,490,491],{"class":199,"line":278},[197,492,493],{}," if original is None:\n",[197,495,496],{"class":199,"line":284},[197,497,498],{}," sys.modules.pop(target_module_name, None)\n",[197,500,501],{"class":199,"line":290},[197,502,503],{}," else:\n",[197,505,506],{"class":199,"line":296},[197,507,508],{}," sys.modules[target_module_name] = original\n",[197,510,511],{"class":199,"line":302},[197,512,228],{"emptyLinePlaceholder":227},[197,514,515],{"class":199,"line":308},[197,516,517],{},"class TestInterpreterIsolation:\n",[197,519,520],{"class":199,"line":314},[197,521,522],{}," def test_temporary_module_swap(self):\n",[197,524,525],{"class":199,"line":320},[197,526,527],{}," class FakeLogger:\n",[197,529,530],{"class":199,"line":326},[197,531,532],{}," @staticmethod\n",[197,534,535],{"class":199,"line":331},[197,536,537],{}," def info(msg): pass\n",[197,539,540],{"class":199,"line":337},[197,541,228],{"emptyLinePlaceholder":227},[197,543,544],{"class":199,"line":343},[197,545,546],{}," with patch_sys_module(\"logging\", FakeLogger):\n",[197,548,550],{"class":199,"line":549},26,[197,551,552],{}," import logging\n",[197,554,556],{"class":199,"line":555},27,[197,557,558],{}," assert logging.info.__name__ == \"info\"\n",[197,560,562],{"class":199,"line":561},28,[197,563,564],{}," # Production code using logging will now hit FakeLogger\n",[197,566,568],{"class":199,"line":567},29,[197,569,311],{},[197,571,573],{"class":199,"line":572},30,[197,574,575],{}," # Restoration verified\n",[197,577,579],{"class":199,"line":578},31,[197,580,552],{},[197,582,584],{"class":199,"line":583},32,[197,585,586],{}," assert hasattr(logging, \"getLogger\")\n",[19,588,589,590,594,595,597,598,601,602,153],{},"For comprehensive isolation patterns, refer to ",[26,591,593],{"href":592},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-builtins-and-sys-modules-safely\u002F","Patching builtins and sys modules safely",". When running under ",[42,596,394],{},", mark global patch tests with ",[42,599,600],{},"@pytest.mark.xdist_group(\"global_patches\")"," to serialize execution and prevent worker crashes. Always verify restoration in teardown by asserting ",[42,603,604],{},"sys.modules[target] is original",[14,606,608],{"id":607},"asynchronous-boundaries-and-event-loop-patching","Asynchronous Boundaries and Event Loop Patching",[19,610,611,612,614,615,618,619,622,623,626],{},"Asynchronous Python introduces unique patching challenges centered around event loop lifecycle management and awaitable resolution. Standard ",[42,613,145],{}," objects do not implement the ",[42,616,617],{},"__await__"," protocol, causing ",[42,620,621],{},"TypeError: object MagicMock can't be used in 'await' expression"," when patched coroutines are awaited. ",[42,624,625],{},"unittest.mock.AsyncMock"," solves this by automatically returning awaitable proxies and tracking coroutine call history.",[19,628,629,630,633,634,637,638,641,642,104,645,648,649,652],{},"Patching ",[42,631,632],{},"asyncio.create_task",", ",[42,635,636],{},"asyncio.gather",", or ",[42,639,640],{},"asyncio.wait"," requires careful consideration of task scheduling. Replacing these functions with synchronous mocks breaks the event loop's internal state machine, leading to ",[42,643,644],{},"RuntimeError: Event loop is closed",[42,646,647],{},"Task was destroyed but it is pending!",". The correct approach is to patch at the application boundary rather than the scheduler level, or to use ",[42,650,651],{},"AsyncMock"," that properly yields control back to the loop.",[19,654,655,656,633,659,662,663,666,667,669,670,104,673,676,677,680,681,684,685,688],{},"Async context managers and async generators require explicit ",[42,657,658],{},"__aenter__",[42,660,661],{},"__aexit__",", and ",[42,664,665],{},"__aiter__"," implementations. ",[42,668,651],{}," handles these automatically when configured with ",[42,671,672],{},"return_value",[42,674,675],{},"side_effect"," as async callables. Fixture scoping is critical: ",[42,678,679],{},"pytest-asyncio"," provides ",[42,682,683],{},"@pytest.fixture"," with ",[42,686,687],{},"scope=\"function\""," by default, but event loop fixtures must be explicitly scoped to avoid cross-test loop pollution.",[188,690,692],{"className":190,"code":691,"language":192,"meta":193,"style":193},"import pytest\nimport asyncio\nfrom unittest.mock import AsyncMock, patch\nfrom my_app.network import AsyncHTTPClient\n\n@pytest.mark.asyncio\nclass TestAsyncPatching:\n async def test_async_context_manager_patching(self):\n # AsyncMock automatically handles __aenter__\u002F__aexit__\n mock_client = AsyncMock()\n mock_client.__aenter__.return_value = mock_client\n mock_client.get = AsyncMock(return_value={\"status\": 200})\n\n with patch.object(AsyncHTTPClient, \"connect\", return_value=mock_client):\n async with AsyncHTTPClient() as client:\n response = await client.get(\"\u002Fhealth\")\n assert response[\"status\"] == 200\n \n mock_client.__aenter__.assert_awaited_once()\n mock_client.get.assert_awaited_once_with(\"\u002Fhealth\")\n\n async def test_task_creation_isolation(self):\n # Patch create_task to return a pre-resolved coroutine\n async def fake_task():\n return \"completed\"\n \n with patch.object(asyncio, \"create_task\", return_value=asyncio.ensure_future(fake_task())) as mock_create:\n task = asyncio.create_task(fake_task())\n result = await task\n assert result == \"completed\"\n mock_create.assert_called_once()\n",[42,693,694,698,703,708,713,717,722,727,732,737,742,747,752,756,761,766,771,776,780,785,790,794,799,804,809,814,818,823,828,833,838],{"__ignoreMap":193},[197,695,696],{"class":199,"line":200},[197,697,203],{},[197,699,700],{"class":199,"line":206},[197,701,702],{},"import asyncio\n",[197,704,705],{"class":199,"line":212},[197,706,707],{},"from unittest.mock import AsyncMock, patch\n",[197,709,710],{"class":199,"line":218},[197,711,712],{},"from my_app.network import AsyncHTTPClient\n",[197,714,715],{"class":199,"line":224},[197,716,228],{"emptyLinePlaceholder":227},[197,718,719],{"class":199,"line":231},[197,720,721],{},"@pytest.mark.asyncio\n",[197,723,724],{"class":199,"line":237},[197,725,726],{},"class TestAsyncPatching:\n",[197,728,729],{"class":199,"line":243},[197,730,731],{}," async def test_async_context_manager_patching(self):\n",[197,733,734],{"class":199,"line":249},[197,735,736],{}," # AsyncMock automatically handles __aenter__\u002F__aexit__\n",[197,738,739],{"class":199,"line":255},[197,740,741],{}," mock_client = AsyncMock()\n",[197,743,744],{"class":199,"line":261},[197,745,746],{}," mock_client.__aenter__.return_value = mock_client\n",[197,748,749],{"class":199,"line":266},[197,750,751],{}," mock_client.get = AsyncMock(return_value={\"status\": 200})\n",[197,753,754],{"class":199,"line":272},[197,755,228],{"emptyLinePlaceholder":227},[197,757,758],{"class":199,"line":278},[197,759,760],{}," with patch.object(AsyncHTTPClient, \"connect\", return_value=mock_client):\n",[197,762,763],{"class":199,"line":284},[197,764,765],{}," async with AsyncHTTPClient() as client:\n",[197,767,768],{"class":199,"line":290},[197,769,770],{}," response = await client.get(\"\u002Fhealth\")\n",[197,772,773],{"class":199,"line":296},[197,774,775],{}," assert response[\"status\"] == 200\n",[197,777,778],{"class":199,"line":302},[197,779,311],{},[197,781,782],{"class":199,"line":308},[197,783,784],{}," mock_client.__aenter__.assert_awaited_once()\n",[197,786,787],{"class":199,"line":314},[197,788,789],{}," mock_client.get.assert_awaited_once_with(\"\u002Fhealth\")\n",[197,791,792],{"class":199,"line":320},[197,793,228],{"emptyLinePlaceholder":227},[197,795,796],{"class":199,"line":326},[197,797,798],{}," async def test_task_creation_isolation(self):\n",[197,800,801],{"class":199,"line":331},[197,802,803],{}," # Patch create_task to return a pre-resolved coroutine\n",[197,805,806],{"class":199,"line":337},[197,807,808],{}," async def fake_task():\n",[197,810,811],{"class":199,"line":343},[197,812,813],{}," return \"completed\"\n",[197,815,816],{"class":199,"line":549},[197,817,311],{},[197,819,820],{"class":199,"line":555},[197,821,822],{}," with patch.object(asyncio, \"create_task\", return_value=asyncio.ensure_future(fake_task())) as mock_create:\n",[197,824,825],{"class":199,"line":561},[197,826,827],{}," task = asyncio.create_task(fake_task())\n",[197,829,830],{"class":199,"line":567},[197,831,832],{}," result = await task\n",[197,834,835],{"class":199,"line":572},[197,836,837],{}," assert result == \"completed\"\n",[197,839,840],{"class":199,"line":578},[197,841,842],{}," mock_create.assert_called_once()\n",[19,844,845,846,684,848,851,852,855],{},"For advanced awaitable mock patterns and loop injection strategies, consult Testing asyncio tasks with mock.patch. Always configure ",[42,847,679],{},[42,849,850],{},"asyncio_mode = \"auto\""," in ",[42,853,854],{},"pyproject.toml"," to eliminate manual loop management. Avoid patching the loop itself; instead, mock the I\u002FO boundaries and let the event loop execute normally.",[14,857,859],{"id":858},"external-integration-and-network-layer-isolation","External Integration and Network Layer Isolation",[19,861,862,863,633,866,869],{},"Unit tests must never traverse network boundaries. Patching at the HTTP client level (",[42,864,865],{},"requests.get",[42,867,868],{},"httpx.AsyncClient",") provides isolation but often misses transport-layer nuances like connection pooling, TLS verification, and retry logic. Modern architectures benefit from patching at the adapter or transport layer, ensuring all outgoing requests are intercepted regardless of the high-level client implementation.",[19,871,111,872,875,876,879,880,633,883,886,887,633,890,892],{},[42,873,874],{},"responses"," library intercepts HTTP calls at the ",[42,877,878],{},"urllib3"," level, capturing requests made by ",[42,881,882],{},"requests",[42,884,885],{},"httpx",", and other compatible libraries. This approach eliminates the need to patch multiple client methods and provides deterministic response sequencing, timeout simulation, and retry backoff testing. When combined with ",[42,888,889],{},"pytest",[42,891,874],{}," integrates seamlessly via fixtures that activate and deactivate interceptors automatically.",[19,894,895,896,104,899,902],{},"Deterministic response sequencing is critical for testing idempotency and error recovery. By queuing multiple responses with varying status codes and payloads, you can simulate flaky upstream services and verify client resilience. Timeout simulation requires patching the underlying socket or using library-specific timeout parameters to trigger ",[42,897,898],{},"requests.exceptions.Timeout",[42,900,901],{},"httpx.ReadTimeout"," without introducing real delays.",[188,904,906],{"className":190,"code":905,"language":192,"meta":193,"style":193},"import pytest\nimport responses\nimport httpx\nfrom my_app.services import ExternalAPI\n\n@responses.activate\ndef test_http_adapter_interception():\n # Queue responses for retry\u002Fbackoff testing\n responses.add(responses.GET, \"https:\u002F\u002Fapi.example.com\u002Fdata\", json={\"error\": \"rate_limit\"}, status=429)\n responses.add(responses.GET, \"https:\u002F\u002Fapi.example.com\u002Fdata\", json={\"data\": \"success\"}, status=200)\n \n client = httpx.Client()\n api = ExternalAPI(client=client)\n \n # First call hits 429, second hits 200\n result = api.fetch_with_retry(\"https:\u002F\u002Fapi.example.com\u002Fdata\")\n assert result == {\"data\": \"success\"}\n \n # Verify transport layer interception\n assert len(responses.calls) == 2\n assert responses.calls[0].request.url == \"https:\u002F\u002Fapi.example.com\u002Fdata\"\n assert responses.calls[0].response.status_code == 429\n\n@pytest.mark.asyncio\nasync def test_async_httpx_transport():\n # httpx transport mocking via AsyncMock\n mock_transport = AsyncMock()\n mock_response = httpx.Response(200, json={\"id\": 1})\n mock_transport.handle_async_request = AsyncMock(return_value=mock_response)\n \n async with httpx.AsyncClient(transport=mock_transport) as client:\n resp = await client.get(\"https:\u002F\u002Fapi.example.com\")\n assert resp.status_code == 200\n",[42,907,908,912,917,922,927,931,936,941,946,951,956,960,965,970,974,979,984,989,993,998,1003,1008,1013,1017,1021,1026,1031,1036,1041,1046,1050,1055,1060],{"__ignoreMap":193},[197,909,910],{"class":199,"line":200},[197,911,203],{},[197,913,914],{"class":199,"line":206},[197,915,916],{},"import responses\n",[197,918,919],{"class":199,"line":212},[197,920,921],{},"import httpx\n",[197,923,924],{"class":199,"line":218},[197,925,926],{},"from my_app.services import ExternalAPI\n",[197,928,929],{"class":199,"line":224},[197,930,228],{"emptyLinePlaceholder":227},[197,932,933],{"class":199,"line":231},[197,934,935],{},"@responses.activate\n",[197,937,938],{"class":199,"line":237},[197,939,940],{},"def test_http_adapter_interception():\n",[197,942,943],{"class":199,"line":243},[197,944,945],{}," # Queue responses for retry\u002Fbackoff testing\n",[197,947,948],{"class":199,"line":249},[197,949,950],{}," responses.add(responses.GET, \"https:\u002F\u002Fapi.example.com\u002Fdata\", json={\"error\": \"rate_limit\"}, status=429)\n",[197,952,953],{"class":199,"line":255},[197,954,955],{}," responses.add(responses.GET, \"https:\u002F\u002Fapi.example.com\u002Fdata\", json={\"data\": \"success\"}, status=200)\n",[197,957,958],{"class":199,"line":261},[197,959,311],{},[197,961,962],{"class":199,"line":266},[197,963,964],{}," client = httpx.Client()\n",[197,966,967],{"class":199,"line":272},[197,968,969],{}," api = ExternalAPI(client=client)\n",[197,971,972],{"class":199,"line":278},[197,973,311],{},[197,975,976],{"class":199,"line":284},[197,977,978],{}," # First call hits 429, second hits 200\n",[197,980,981],{"class":199,"line":290},[197,982,983],{}," result = api.fetch_with_retry(\"https:\u002F\u002Fapi.example.com\u002Fdata\")\n",[197,985,986],{"class":199,"line":296},[197,987,988],{}," assert result == {\"data\": \"success\"}\n",[197,990,991],{"class":199,"line":302},[197,992,311],{},[197,994,995],{"class":199,"line":308},[197,996,997],{}," # Verify transport layer interception\n",[197,999,1000],{"class":199,"line":314},[197,1001,1002],{}," assert len(responses.calls) == 2\n",[197,1004,1005],{"class":199,"line":320},[197,1006,1007],{}," assert responses.calls[0].request.url == \"https:\u002F\u002Fapi.example.com\u002Fdata\"\n",[197,1009,1010],{"class":199,"line":326},[197,1011,1012],{}," assert responses.calls[0].response.status_code == 429\n",[197,1014,1015],{"class":199,"line":331},[197,1016,228],{"emptyLinePlaceholder":227},[197,1018,1019],{"class":199,"line":337},[197,1020,721],{},[197,1022,1023],{"class":199,"line":343},[197,1024,1025],{},"async def test_async_httpx_transport():\n",[197,1027,1028],{"class":199,"line":549},[197,1029,1030],{}," # httpx transport mocking via AsyncMock\n",[197,1032,1033],{"class":199,"line":555},[197,1034,1035],{}," mock_transport = AsyncMock()\n",[197,1037,1038],{"class":199,"line":561},[197,1039,1040],{}," mock_response = httpx.Response(200, json={\"id\": 1})\n",[197,1042,1043],{"class":199,"line":567},[197,1044,1045],{}," mock_transport.handle_async_request = AsyncMock(return_value=mock_response)\n",[197,1047,1048],{"class":199,"line":572},[197,1049,311],{},[197,1051,1052],{"class":199,"line":578},[197,1053,1054],{}," async with httpx.AsyncClient(transport=mock_transport) as client:\n",[197,1056,1057],{"class":199,"line":583},[197,1058,1059],{}," resp = await client.get(\"https:\u002F\u002Fapi.example.com\")\n",[197,1061,1063],{"class":199,"line":1062},33,[197,1064,1065],{}," assert resp.status_code == 200\n",[19,1067,1068,1069,1072,1073,1076,1077,1080],{},"For comprehensive HTTP interception patterns, see Mocking external APIs with responses library. In CI environments, enforce network isolation by setting ",[42,1070,1071],{},"HTTP_PROXY"," to ",[42,1074,1075],{},"localhost:0"," and using ",[42,1078,1079],{},"pytest --strict-markers"," to flag tests that bypass mock interceptors. Always validate that retry logic respects exponential backoff and circuit breaker thresholds without introducing real latency.",[14,1082,1084],{"id":1083},"architectural-patterns-for-maintainable-patching","Architectural Patterns for Maintainable Patching",[19,1086,1087,1088,1091],{},"Scaling patching strategies across large codebases requires architectural discipline. Ad-hoc patches scattered throughout test files create maintenance bottlenecks and obscure failure origins. Production-grade suites centralize patching logic through reusable factories, fixture-based scoping, and ",[42,1089,1090],{},"conftest.py"," organization.",[19,1093,1094,1095,1097,1098,1100,1101,1103,1104,1106],{},"Fixture-scoped patch lifecycles provide deterministic setup and teardown. A ",[42,1096,107],{},"-scoped fixture applies patches once per test file, reducing overhead for expensive introspection. A ",[42,1099,103],{},"-scoped fixture ensures isolation between tests. Combine scopes strategically: use ",[42,1102,107],{}," for stable third-party libraries and ",[42,1105,103],{}," for volatile application state.",[19,1108,1109,1110,1113],{},"Parametrized tests with dynamic mocks require careful fixture design. When testing multiple configurations, generate mock instances inside the fixture and inject them via ",[42,1111,1112],{},"pytest.param",". This avoids patch collision and ensures each parameter set receives a clean mock state. Circular dependencies can be resolved by patching at the import boundary rather than the instantiation point, breaking the dependency cycle during test collection.",[19,1115,1116,1117,1120,1121,1124,1125,1127,1128,1131,1132,1134,1135,1138],{},"Profiling patch overhead is essential for CI velocity. Use ",[42,1118,1119],{},"pytest-profiling"," to identify slow test collection phases. High ",[42,1122,1123],{},"patch"," overhead typically indicates excessive ",[42,1126,162],{}," recursion or unscoped global patches. Cache mock instances in fixtures, limit ",[42,1129,1130],{},"create_autospec"," to public APIs, and prefer ",[42,1133,178],{}," for internal modules. Monitor test execution with ",[42,1136,1137],{},"pytest --durations=10"," to pinpoint bottlenecks.",[188,1140,1142],{"className":190,"code":1141,"language":192,"meta":193,"style":193},"# conftest.py\nimport pytest\nfrom unittest.mock import patch, MagicMock\n\n@pytest.fixture(scope=\"module\")\ndef patched_external_service():\n \"\"\"Module-scoped patch for stable third-party dependency.\"\"\"\n mock_svc = MagicMock(spec=[\"fetch\", \"update\"])\n mock_svc.fetch.return_value = {\"status\": \"ok\"}\n with patch(\"my_app.services.ExternalService\", return_value=mock_svc):\n yield mock_svc\n\n@pytest.fixture\ndef dynamic_mock_factory():\n \"\"\"Factory for parametrized test isolation.\"\"\"\n def create_mock(config):\n mock = MagicMock()\n mock.configure_mock(**config)\n return mock\n return create_mock\n\n# test_integration.py\nclass TestParametrizedPatching:\n @pytest.mark.parametrize(\"config,expected\", [\n ({\"timeout\": 5.0}, 5.0),\n ({\"timeout\": 30.0}, 30.0),\n ])\n def test_dynamic_mock_injection(self, dynamic_mock_factory, config, expected):\n mock = dynamic_mock_factory(config)\n assert mock.timeout == expected\n",[42,1143,1144,1149,1153,1158,1162,1167,1172,1177,1182,1187,1192,1197,1201,1206,1211,1216,1221,1226,1231,1236,1241,1245,1250,1255,1260,1265,1270,1275,1280,1285],{"__ignoreMap":193},[197,1145,1146],{"class":199,"line":200},[197,1147,1148],{},"# conftest.py\n",[197,1150,1151],{"class":199,"line":206},[197,1152,203],{},[197,1154,1155],{"class":199,"line":212},[197,1156,1157],{},"from unittest.mock import patch, MagicMock\n",[197,1159,1160],{"class":199,"line":218},[197,1161,228],{"emptyLinePlaceholder":227},[197,1163,1164],{"class":199,"line":224},[197,1165,1166],{},"@pytest.fixture(scope=\"module\")\n",[197,1168,1169],{"class":199,"line":231},[197,1170,1171],{},"def patched_external_service():\n",[197,1173,1174],{"class":199,"line":237},[197,1175,1176],{}," \"\"\"Module-scoped patch for stable third-party dependency.\"\"\"\n",[197,1178,1179],{"class":199,"line":243},[197,1180,1181],{}," mock_svc = MagicMock(spec=[\"fetch\", \"update\"])\n",[197,1183,1184],{"class":199,"line":249},[197,1185,1186],{}," mock_svc.fetch.return_value = {\"status\": \"ok\"}\n",[197,1188,1189],{"class":199,"line":255},[197,1190,1191],{}," with patch(\"my_app.services.ExternalService\", return_value=mock_svc):\n",[197,1193,1194],{"class":199,"line":261},[197,1195,1196],{}," yield mock_svc\n",[197,1198,1199],{"class":199,"line":266},[197,1200,228],{"emptyLinePlaceholder":227},[197,1202,1203],{"class":199,"line":272},[197,1204,1205],{},"@pytest.fixture\n",[197,1207,1208],{"class":199,"line":278},[197,1209,1210],{},"def dynamic_mock_factory():\n",[197,1212,1213],{"class":199,"line":284},[197,1214,1215],{}," \"\"\"Factory for parametrized test isolation.\"\"\"\n",[197,1217,1218],{"class":199,"line":290},[197,1219,1220],{}," def create_mock(config):\n",[197,1222,1223],{"class":199,"line":296},[197,1224,1225],{}," mock = MagicMock()\n",[197,1227,1228],{"class":199,"line":302},[197,1229,1230],{}," mock.configure_mock(**config)\n",[197,1232,1233],{"class":199,"line":308},[197,1234,1235],{}," return mock\n",[197,1237,1238],{"class":199,"line":314},[197,1239,1240],{}," return create_mock\n",[197,1242,1243],{"class":199,"line":320},[197,1244,228],{"emptyLinePlaceholder":227},[197,1246,1247],{"class":199,"line":326},[197,1248,1249],{},"# test_integration.py\n",[197,1251,1252],{"class":199,"line":331},[197,1253,1254],{},"class TestParametrizedPatching:\n",[197,1256,1257],{"class":199,"line":337},[197,1258,1259],{}," @pytest.mark.parametrize(\"config,expected\", [\n",[197,1261,1262],{"class":199,"line":343},[197,1263,1264],{}," ({\"timeout\": 5.0}, 5.0),\n",[197,1266,1267],{"class":199,"line":549},[197,1268,1269],{}," ({\"timeout\": 30.0}, 30.0),\n",[197,1271,1272],{"class":199,"line":555},[197,1273,1274],{}," ])\n",[197,1276,1277],{"class":199,"line":561},[197,1278,1279],{}," def test_dynamic_mock_injection(self, dynamic_mock_factory, config, expected):\n",[197,1281,1282],{"class":199,"line":567},[197,1283,1284],{}," mock = dynamic_mock_factory(config)\n",[197,1286,1287],{"class":199,"line":572},[197,1288,1289],{}," assert mock.timeout == expected\n",[19,1291,1292,1293,1295],{},"Organize ",[42,1294,1090],{}," hierarchically to match package structure. Root-level fixtures handle global interceptors; subdirectory fixtures manage domain-specific patches. Document patch boundaries clearly in docstrings to prevent accidental overrides. This architectural approach transforms patching from a tactical workaround into a scalable testing infrastructure.",[14,1297,1299],{"id":1298},"common-anti-patterns-and-remediation-strategies","Common Anti-Patterns and Remediation Strategies",[1301,1302,1303,1319],"table",{},[1304,1305,1306],"thead",{},[1307,1308,1309,1313,1316],"tr",{},[1310,1311,1312],"th",{},"Anti-Pattern",[1310,1314,1315],{},"Consequence",[1310,1317,1318],{},"Remediation",[1320,1321,1322,1334,1357,1377],"tbody",{},[1307,1323,1324,1328,1331],{},[1325,1326,1327],"td",{},"Patching the definition module instead of the usage module",[1325,1329,1330],{},"Tests pass but production code executes real logic, causing false confidence and integration failures",[1325,1332,1333],{},"Always trace the import path in the consuming module and patch the fully qualified name where it is referenced",[1307,1335,1336,1344,1347],{},[1325,1337,1338,1339,1341,1342],{},"Using ",[42,1340,182],{}," without ",[42,1343,162],{},[1325,1345,1346],{},"Silent typos in mock attributes go undetected, leading to brittle tests that break on refactoring",[1325,1348,1349,1350,1352,1353,1356],{},"Enforce ",[42,1351,158],{}," or use strict mode in ",[42,1354,1355],{},"pytest-mock"," to catch attribute access violations",[1307,1358,1359,1365,1371],{},[1325,1360,1361,1362,1364],{},"Global patching in ",[42,1363,1090],{}," without proper teardown",[1325,1366,1367,1368,1370],{},"State leakage between tests, flaky CI runs, and ",[42,1369,394],{}," worker crashes",[1325,1372,1373,1374,1376],{},"Scope patches to function or module level using pytest fixtures with explicit ",[42,1375,408],{},"\u002Fteardown blocks",[1307,1378,1379,1385,1388],{},[1325,1380,1381,1382,1384],{},"Patching builtins or ",[42,1383,363],{}," without thread\u002Fprocess isolation",[1325,1386,1387],{},"Race conditions in parallel test execution, corrupted interpreter state",[1325,1389,1390,1391,1393],{},"Use ",[42,1392,394],{}," compatible isolation wrappers or restrict parallel execution for global patch tests via markers",[14,1395,1397],{"id":1396},"frequently-asked-questions","Frequently Asked Questions",[19,1399,1400,1408,1409,1411],{},[1401,1402,1403,1404,1407],"strong",{},"When should I use ",[42,1405,1406],{},"patch()"," versus dependency injection for test isolation?","\nUse ",[42,1410,1406],{}," for third-party libraries, legacy code, or system-level dependencies where refactoring is impractical or economically prohibitive. Prefer dependency injection for internal modules to improve architecture, eliminate patching overhead, and enable compile-time interface validation. Patching is a tactical bridge; injection is a strategic destination.",[19,1413,1414,1420,1421,1423,1424,1426,1427,1429,1430,1432,1433,1435],{},[1401,1415,1416,1417,1419],{},"How do I prevent ",[42,1418,162],{}," from slowing down my test suite?","\nLimit ",[42,1422,162],{}," to boundary modules and use ",[42,1425,178],{}," for known interfaces. Cache mock instances in fixtures rather than recreating them per test. Avoid recursive ",[42,1428,162],{}," on deeply nested object graphs by patching at the import boundary. Profile collection time with ",[42,1431,1119],{}," and replace heavy specs with lightweight ",[42,1434,145],{}," where strict validation isn't required.",[19,1437,1438,1447,1448,1450,1451,1453,1454,1457,1458,1461],{},[1401,1439,1440,1441,1443,1444,1446],{},"Why does patching ",[42,1442,96],{}," cause flaky tests in ",[42,1445,394],{},"?","\nEach worker process maintains its own ",[42,1449,96],{}," cache. Concurrent mutations without synchronization lead to race conditions where one worker's patch overwrites another's import resolution. Use process-scoped fixtures or isolate ",[42,1452,363],{}," patching to single-worker execution via ",[42,1455,1456],{},"@pytest.mark.xdist_group(\"sys_patches\")",". Always verify restoration in ",[42,1459,1460],{},"finally"," blocks.",[19,1463,1464,1469,1470,384,1472,1474,1475,1477,1478,1480,1481,1483,1484,1487,1488,1490],{},[1401,1465,1466,1467,1446],{},"Can I mock async context managers without ",[42,1468,651],{},"\nYes, by implementing ",[42,1471,658],{},[42,1473,661],{}," as async methods on a ",[42,1476,145],{},", but ",[42,1479,651],{}," is strongly recommended for automatic awaitable resolution and proper coroutine tracking. Manual implementation requires explicit ",[42,1482,672],{}," configuration and risks missing ",[42,1485,1486],{},"await"," protocol nuances that ",[42,1489,651],{}," handles natively.",[1492,1493,1494],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":193,"searchDepth":206,"depth":206,"links":1496},[1497,1498,1499,1500,1501,1502,1503,1504,1505],{"id":16,"depth":206,"text":17},{"id":36,"depth":206,"text":37},{"id":138,"depth":206,"text":139},{"id":356,"depth":206,"text":357},{"id":607,"depth":206,"text":608},{"id":858,"depth":206,"text":859},{"id":1083,"depth":206,"text":1084},{"id":1298,"depth":206,"text":1299},{"id":1396,"depth":206,"text":1397},"md",{},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases",{"title":5,"description":193},"advanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Findex","BQO1QmuSU9s45l24wetGAMOAHz_vYirHdnWfucPiBZM",1778004577656]