[{"data":1,"prerenderedAt":1023},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fstacking-multiple-patches-without-argument-confusion\u002F":3},{"id":4,"title":5,"body":6,"description":986,"extension":987,"meta":988,"navigation":99,"path":1019,"seo":1020,"stem":1021,"__hash__":1022},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fstacking-multiple-patches-without-argument-confusion\u002Findex.md","Stacking Multiple Patches Without Argument Confusion",{"type":7,"value":8,"toc":975},"minimark",[9,26,33,40,45,67,71,136,212,264,410,414,417,431,447,451,500,504,507,602,621,712,716,723,794,803,807,810,813,881,889,893,911,926,932,936,966,971],[10,11,12,13,17,18,21,22,25],"p",{},"Three stacked ",[14,15,16],"code",{},"@patch"," decorators and a test signature of ",[14,19,20],{},"(self, mock_mail, mock_db, mock_clock)"," look tidy until someone reorders the decorators — or writes them in the order that seems natural. Decorators apply bottom-up, so the bottom decorator's mock is the first argument, and a test that assumes top-down configures ",[14,23,24],{},"mock_mail"," with the database's return value. Nothing fails at that point; the test simply stops testing what it claims to.",[10,27,28,29,32],{},"The confusion is mechanical and avoidable. Named context managers keep each patch beside the variable it binds, fixtures give commonly patched collaborators stable names, and ",[14,30,31],{},"ExitStack"," handles variable-length patch sets. The more interesting observation is that a test needing many patches is usually telling you something about the code rather than about the test.",[10,34,35,36,39],{},"The ordering bug deserves attention because of how it fails. A test whose mocks are swapped rarely errors; it runs, configures the wrong double, and then asserts on a double that was never touched by the code path in question. The assertion might pass vacuously — ",[14,37,38],{},"assert_not_called"," on the wrong mock is trivially true — or fail with a message that makes no sense until someone counts decorators. Either way, the test has quietly stopped meaning what its name says, which is the worst outcome a test can have.",[41,42,44],"h2",{"id":43},"prerequisites","Prerequisites",[46,47,48,52,58],"ul",{},[49,50,51],"li",{},"Python 3.10+ for parenthesised context managers; 3.8+ for everything else.",[49,53,54,57],{},[14,55,56],{},"pytest >= 8.0"," if using fixtures.",[49,59,60,61,66],{},"The target rules from ",[62,63,65],"a",{"href":64},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fwhere-to-patch-understanding-mock-patch-targets\u002F","where to patch",".",[41,68,70],{"id":69},"solution","Solution",[72,73,78],"pre",{"className":74,"code":75,"language":76,"meta":77,"style":77},"language-python shiki shiki-themes github-light github-dark","# Fragile: bottom-up argument order, easy to get backwards.\nfrom unittest.mock import patch\n\n\n@patch(\"myapp.orders.send_email\")         # third argument\n@patch(\"myapp.orders.OrderRepository\")    # second argument\n@patch(\"myapp.orders.utcnow\")             # FIRST argument: closest to the function\ndef test_order_confirmation(mock_utcnow, mock_repo, mock_send):\n    ...\n","python","",[14,79,80,88,94,101,106,112,118,124,130],{"__ignoreMap":77},[81,82,85],"span",{"class":83,"line":84},"line",1,[81,86,87],{},"# Fragile: bottom-up argument order, easy to get backwards.\n",[81,89,91],{"class":83,"line":90},2,[81,92,93],{},"from unittest.mock import patch\n",[81,95,97],{"class":83,"line":96},3,[81,98,100],{"emptyLinePlaceholder":99},true,"\n",[81,102,104],{"class":83,"line":103},4,[81,105,100],{"emptyLinePlaceholder":99},[81,107,109],{"class":83,"line":108},5,[81,110,111],{},"@patch(\"myapp.orders.send_email\")         # third argument\n",[81,113,115],{"class":83,"line":114},6,[81,116,117],{},"@patch(\"myapp.orders.OrderRepository\")    # second argument\n",[81,119,121],{"class":83,"line":120},7,[81,122,123],{},"@patch(\"myapp.orders.utcnow\")             # FIRST argument: closest to the function\n",[81,125,127],{"class":83,"line":126},8,[81,128,129],{},"def test_order_confirmation(mock_utcnow, mock_repo, mock_send):\n",[81,131,133],{"class":83,"line":132},9,[81,134,135],{},"    ...\n",[72,137,139],{"className":74,"code":138,"language":76,"meta":77,"style":77},"# Robust: each patch named where it is declared (Python 3.10+).\nfrom unittest.mock import patch\n\n\ndef test_order_confirmation():\n    with (\n        patch(\"myapp.orders.utcnow\", return_value=FIXED_NOW) as utcnow,\n        patch(\"myapp.orders.OrderRepository\", autospec=True) as repo_cls,\n        patch(\"myapp.orders.send_email\", autospec=True) as send_email,\n    ):\n        confirm_order(\"ord_1\")\n\n    send_email.assert_called_once()\n    repo_cls.return_value.mark_confirmed.assert_called_once_with(\"ord_1\")\n",[14,140,141,146,150,154,158,163,168,173,178,183,189,195,200,206],{"__ignoreMap":77},[81,142,143],{"class":83,"line":84},[81,144,145],{},"# Robust: each patch named where it is declared (Python 3.10+).\n",[81,147,148],{"class":83,"line":90},[81,149,93],{},[81,151,152],{"class":83,"line":96},[81,153,100],{"emptyLinePlaceholder":99},[81,155,156],{"class":83,"line":103},[81,157,100],{"emptyLinePlaceholder":99},[81,159,160],{"class":83,"line":108},[81,161,162],{},"def test_order_confirmation():\n",[81,164,165],{"class":83,"line":114},[81,166,167],{},"    with (\n",[81,169,170],{"class":83,"line":120},[81,171,172],{},"        patch(\"myapp.orders.utcnow\", return_value=FIXED_NOW) as utcnow,\n",[81,174,175],{"class":83,"line":126},[81,176,177],{},"        patch(\"myapp.orders.OrderRepository\", autospec=True) as repo_cls,\n",[81,179,180],{"class":83,"line":132},[81,181,182],{},"        patch(\"myapp.orders.send_email\", autospec=True) as send_email,\n",[81,184,186],{"class":83,"line":185},10,[81,187,188],{},"    ):\n",[81,190,192],{"class":83,"line":191},11,[81,193,194],{},"        confirm_order(\"ord_1\")\n",[81,196,198],{"class":83,"line":197},12,[81,199,100],{"emptyLinePlaceholder":99},[81,201,203],{"class":83,"line":202},13,[81,204,205],{},"    send_email.assert_called_once()\n",[81,207,209],{"class":83,"line":208},14,[81,210,211],{},"    repo_cls.return_value.mark_confirmed.assert_called_once_with(\"ord_1\")\n",[72,213,215],{"className":74,"code":214,"language":76,"meta":77,"style":77},"# Several attributes of one module in a single call.\nfrom unittest.mock import DEFAULT, patch\n\n\ndef test_notifications_are_sent():\n    with patch.multiple(\"myapp.notify\", send_email=DEFAULT, send_sms=DEFAULT) as mocks:\n        notify_customer(\"cus_1\", \"shipped\")\n\n    mocks[\"send_email\"].assert_called_once()\n    mocks[\"send_sms\"].assert_not_called()\n",[14,216,217,222,227,231,235,240,245,250,254,259],{"__ignoreMap":77},[81,218,219],{"class":83,"line":84},[81,220,221],{},"# Several attributes of one module in a single call.\n",[81,223,224],{"class":83,"line":90},[81,225,226],{},"from unittest.mock import DEFAULT, patch\n",[81,228,229],{"class":83,"line":96},[81,230,100],{"emptyLinePlaceholder":99},[81,232,233],{"class":83,"line":103},[81,234,100],{"emptyLinePlaceholder":99},[81,236,237],{"class":83,"line":108},[81,238,239],{},"def test_notifications_are_sent():\n",[81,241,242],{"class":83,"line":114},[81,243,244],{},"    with patch.multiple(\"myapp.notify\", send_email=DEFAULT, send_sms=DEFAULT) as mocks:\n",[81,246,247],{"class":83,"line":120},[81,248,249],{},"        notify_customer(\"cus_1\", \"shipped\")\n",[81,251,252],{"class":83,"line":126},[81,253,100],{"emptyLinePlaceholder":99},[81,255,256],{"class":83,"line":132},[81,257,258],{},"    mocks[\"send_email\"].assert_called_once()\n",[81,260,261],{"class":83,"line":185},[81,262,263],{},"    mocks[\"send_sms\"].assert_not_called()\n",[265,266,269,402],"figure",{"className":267},[268],"diagram",[270,271,278,279,278,283,278,287,278,305,278,313,278,323,278,331,278,337,278,340,278,344,278,347,278,351,278,357,278,361,278,369,278,373,278,377,278,383,278,388,278,391,278,395,278,398],"svg",{"viewBox":272,"role":273,"ariaLabelledBy":274,"xmlns":277},"0 0 820 262","img",[275,276],"stk-t","stk-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[280,281,282],"title",{"id":275},"Why stacked decorators map to arguments bottom-up",[284,285,286],"desc",{"id":276},"Three patch decorators are stacked above a test function. The decorator nearest the function wraps it first and prepends its mock as the first argument; the next decorator wraps that and prepends its mock before, so the topmost decorator's mock ends up last. The argument list therefore reads in reverse of the decorator list.",[288,289,290,291,278],"defs",{},"\n    ",[292,293,300],"marker",{"id":294,"viewBox":295,"refX":296,"refY":297,"markerWidth":298,"markerHeight":298,"orient":299},"stk-a","0 0 10 10","9","5","7","auto-start-reverse",[301,302],"path",{"d":303,"fill":304},"M0 0 L10 5 L0 10 z","#e07a5f",[306,307],"rect",{"x":308,"y":308,"width":309,"height":310,"rx":311,"fill":312},"0","820","262","14","#fffdf8",[314,315,322],"text",{"x":316,"y":317,"textAnchor":318,"fontSize":319,"fontWeight":320,"fill":321},"410","28","middle","16","700","#3d405b","Decorators read top-down; arguments arrive bottom-up",[306,324],{"x":325,"y":326,"width":327,"height":328,"rx":296,"fill":329,"stroke":321,"strokeWidth":330},"26","56","340","40","#f4f1de","1.5",[314,332,336],{"x":333,"y":334,"fontSize":335,"fill":321},"46","81","11.5","@patch(\"…send_email\")",[306,338],{"x":325,"y":339,"width":327,"height":328,"rx":296,"fill":329,"stroke":321,"strokeWidth":330},"104",[314,341,343],{"x":333,"y":342,"fontSize":335,"fill":321},"129","@patch(\"…OrderRepository\")",[306,345],{"x":325,"y":346,"width":327,"height":328,"rx":296,"fill":329,"stroke":321,"strokeWidth":330},"152",[314,348,350],{"x":333,"y":349,"fontSize":335,"fill":321},"177","@patch(\"…utcnow\")",[306,352],{"x":325,"y":353,"width":327,"height":328,"rx":296,"fill":354,"stroke":355,"strokeWidth":356},"200","#f7f0da","#f2cc8f","1.8",[314,358,360],{"x":333,"y":359,"fontSize":335,"fill":321},"225","def test(…)",[83,362],{"x1":363,"y1":364,"x2":365,"y2":366,"stroke":304,"strokeWidth":367,"markerEnd":368},"370","172","470","100","1.7","url(#stk-a)",[83,370],{"x1":363,"y1":371,"x2":365,"y2":372,"stroke":304,"strokeWidth":367,"markerEnd":368},"124","140",[83,374],{"x1":363,"y1":375,"x2":365,"y2":376,"stroke":304,"strokeWidth":367,"markerEnd":368},"76","180",[306,378],{"x":379,"y":380,"width":381,"height":328,"rx":296,"fill":382,"stroke":304,"strokeWidth":356},"476","80","318","#fbe9e3",[314,384,387],{"x":385,"y":386,"fontSize":335,"fill":321},"496","105","arg 1: mock_utcnow",[306,389],{"x":379,"y":390,"width":381,"height":328,"rx":296,"fill":382,"stroke":304,"strokeWidth":356},"120",[314,392,394],{"x":385,"y":393,"fontSize":335,"fill":321},"145","arg 2: mock_repo",[306,396],{"x":379,"y":397,"width":381,"height":328,"rx":296,"fill":382,"stroke":304,"strokeWidth":356},"160",[314,399,401],{"x":385,"y":400,"fontSize":335,"fill":321},"185","arg 3: mock_send",[403,404,405,406,409],"figcaption",{},"The crossing arrows are the whole problem. A named ",[14,407,408],{},"with"," statement removes them by putting each name beside its target.",[41,411,413],{"id":412},"why-this-works","Why this works",[10,415,416],{},"A decorator wraps the function beneath it. The decorator nearest the function wraps it first, and its wrapper prepends that decorator's mock to the arguments. The next decorator up wraps the already-wrapped function and prepends its own mock in front, and so on. The topmost decorator's mock therefore ends up last, and the argument list reads in reverse of the decorator list.",[10,418,419,420,422,423,426,427,430],{},"A ",[14,421,408],{}," statement has no such inversion. Each ",[14,424,425],{},"patch(...) as name"," binds its mock to exactly the name written beside it, in the order written, and the parenthesised form introduced in Python 3.10 makes a long list readable without nested indentation. ",[14,428,429],{},"patch.multiple"," goes further for several attributes of one target, returning a dictionary keyed by attribute name so there is no positional mapping at all.",[10,432,433,434,438,439,442,443,446],{},"The common thread is that every robust alternative binds a mock to a ",[435,436,437],"em",{},"name"," rather than a ",[435,440,441],{},"position",". Positional binding is what decorators force, and it is fragile because nothing connects the position to the target except the reader's ability to count upward. Name-based binding — ",[14,444,445],{},"as",", dictionary keys, fixture names — makes the connection explicit in the code, where a reviewer can see it and a refactoring tool can follow it.",[41,448,450],{"id":449},"edge-cases-and-failure-modes","Edge cases and failure modes",[46,452,453,463,474,480,491],{},[49,454,455,459,460,462],{},[456,457,458],"strong",{},"Decorators plus pytest fixtures."," Mock arguments from ",[14,461,16],{}," come before fixture arguments. Mixing the two makes the signature even harder to read; prefer fixtures or context managers.",[49,464,465,473],{},[456,466,467,469,470,66],{},[14,468,429],{}," without ",[14,471,472],{},"DEFAULT"," Passing a concrete value installs that value, not a mock, and no entry for it appears in the returned dictionary.",[49,475,476,479],{},[456,477,478],{},"Autospec on many targets."," Each autospecced patch imports and inspects its target, which adds up. For large patch sets, fixtures scoped once per test keep it manageable.",[49,481,482,488,489,66],{},[456,483,484,485,487],{},"Nested ",[14,486,408],{}," blocks."," Three levels of indentation for three patches hides the test body. Use the parenthesised form or ",[14,490,31],{},[49,492,493,496,497,499],{},[456,494,495],{},"Class-level decorators."," ",[14,498,16],{}," on a test class applies to every method, with the same bottom-up ordering per method — and only to methods whose names start with the test prefix.",[41,501,503],{"id":502},"fixtures-give-patches-names-that-last","Fixtures give patches names that last",[10,505,506],{},"When the same collaborators are patched across many tests, moving each patch into a fixture solves the ordering problem permanently and removes duplication. Each fixture has a name, tests request the ones they need, and pytest injects them by name rather than position.",[72,508,510],{"className":74,"code":509,"language":76,"meta":77,"style":77},"import pytest\nfrom unittest.mock import patch\n\n\n@pytest.fixture\ndef send_email():\n    with patch(\"myapp.orders.send_email\", autospec=True) as mock:\n        yield mock\n\n\n@pytest.fixture\ndef frozen_now():\n    with patch(\"myapp.orders.utcnow\", return_value=FIXED_NOW) as mock:\n        yield mock\n\n\ndef test_confirmation_email_is_sent(send_email, frozen_now):\n    confirm_order(\"ord_1\")\n    send_email.assert_called_once()\n",[14,511,512,517,521,525,529,534,539,544,549,553,557,561,566,571,575,580,585,591,597],{"__ignoreMap":77},[81,513,514],{"class":83,"line":84},[81,515,516],{},"import pytest\n",[81,518,519],{"class":83,"line":90},[81,520,93],{},[81,522,523],{"class":83,"line":96},[81,524,100],{"emptyLinePlaceholder":99},[81,526,527],{"class":83,"line":103},[81,528,100],{"emptyLinePlaceholder":99},[81,530,531],{"class":83,"line":108},[81,532,533],{},"@pytest.fixture\n",[81,535,536],{"class":83,"line":114},[81,537,538],{},"def send_email():\n",[81,540,541],{"class":83,"line":120},[81,542,543],{},"    with patch(\"myapp.orders.send_email\", autospec=True) as mock:\n",[81,545,546],{"class":83,"line":126},[81,547,548],{},"        yield mock\n",[81,550,551],{"class":83,"line":132},[81,552,100],{"emptyLinePlaceholder":99},[81,554,555],{"class":83,"line":185},[81,556,100],{"emptyLinePlaceholder":99},[81,558,559],{"class":83,"line":191},[81,560,533],{},[81,562,563],{"class":83,"line":197},[81,564,565],{},"def frozen_now():\n",[81,567,568],{"class":83,"line":202},[81,569,570],{},"    with patch(\"myapp.orders.utcnow\", return_value=FIXED_NOW) as mock:\n",[81,572,573],{"class":83,"line":208},[81,574,548],{},[81,576,578],{"class":83,"line":577},15,[81,579,100],{"emptyLinePlaceholder":99},[81,581,583],{"class":83,"line":582},16,[81,584,100],{"emptyLinePlaceholder":99},[81,586,588],{"class":83,"line":587},17,[81,589,590],{},"def test_confirmation_email_is_sent(send_email, frozen_now):\n",[81,592,594],{"class":83,"line":593},18,[81,595,596],{},"    confirm_order(\"ord_1\")\n",[81,598,600],{"class":83,"line":599},19,[81,601,205],{},[10,603,604,605,608,609,612,613,616,617,620],{},"Argument order is now irrelevant — ",[14,606,607],{},"(frozen_now, send_email)"," works identically — and the fixture's name documents what is being replaced. The configuration each patch needs lives in one place, so a change to how the email sender is doubled happens once rather than in every test that patched it. Fixtures can also depend on each other, so a ",[14,610,611],{},"confirmed_order"," fixture that requests ",[14,614,615],{},"send_email"," and ",[14,618,619],{},"frozen_now"," composes a whole scenario from named parts — something stacked decorators cannot express at all without repeating every patch in every test.",[265,622,624,705],{"className":623},[268],[270,625,278,630,278,633,278,636,278,644,278,648,278,653,278,660,278,666,278,672,278,674,278,678,278,682,278,688,278,691,278,698,278,702],{"viewBox":626,"role":273,"ariaLabelledBy":627,"xmlns":277},"0 0 800 236",[628,629],"fx2-t","fx2-d",[280,631,632],{"id":628},"Patches as named fixtures",[284,634,635],{"id":629},"Two fixtures, send_email and frozen_now, each wrap one patch and yield its mock. A test requests them by name in any order, and pytest injects each by name rather than position, so the mapping between mock and target cannot be confused and each patch's configuration lives in one place.",[288,637,290,638,278],{},[292,639,641],{"id":640,"viewBox":295,"refX":296,"refY":297,"markerWidth":298,"markerHeight":298,"orient":299},"fx2-a",[301,642],{"d":303,"fill":643},"#81b29a",[306,645],{"x":308,"y":308,"width":646,"height":647,"rx":311,"fill":312},"800","236",[314,649,652],{"x":650,"y":317,"textAnchor":318,"fontSize":651,"fontWeight":320,"fill":321},"400","15.5","Injected by name, so order cannot matter",[306,654],{"x":325,"y":326,"width":655,"height":656,"rx":657,"fill":658,"stroke":643,"strokeWidth":659},"220","60","10","#e6f0ea","2",[314,661,665],{"x":662,"y":663,"textAnchor":318,"fontSize":664,"fontWeight":320,"fill":321},"136","82","12","send_email fixture",[314,667,671],{"x":662,"y":668,"textAnchor":318,"fontSize":669,"fill":670},"102","11","#2a5f49","patch + autospec, once",[306,673],{"x":325,"y":662,"width":655,"height":656,"rx":657,"fill":658,"stroke":643,"strokeWidth":659},[314,675,677],{"x":662,"y":676,"textAnchor":318,"fontSize":664,"fontWeight":320,"fill":321},"162","frozen_now fixture",[314,679,681],{"x":662,"y":680,"textAnchor":318,"fontSize":669,"fill":670},"182","fixed clock, once",[83,683],{"x1":684,"y1":685,"x2":327,"y2":686,"stroke":643,"strokeWidth":356,"markerEnd":687},"250","86","116","url(#fx2-a)",[83,689],{"x1":684,"y1":690,"x2":327,"y2":662,"stroke":643,"strokeWidth":356,"markerEnd":687},"166",[306,692],{"x":693,"y":694,"width":695,"height":696,"rx":669,"fill":329,"stroke":321,"strokeWidth":697},"346","94","428","64","1.6",[314,699,701],{"x":700,"y":390,"textAnchor":318,"fontSize":664,"fontWeight":320,"fill":321},"560","def test_x(frozen_now, send_email)",[314,703,704],{"x":700,"y":372,"textAnchor":318,"fontSize":669,"fill":321},"any order · names match targets",[403,706,707,708,711],{},"Moving patches into fixtures also makes the patched collaborators discoverable: ",[14,709,710],{},"pytest --fixtures"," lists them with their docstrings.",[41,713,715],{"id":714},"a-variable-number-of-patches","A variable number of patches",[10,717,718,719,722],{},"Occasionally the set of patches is not fixed — a parametrised test that disables a different combination of feature flags per case, or a helper that neutralises every external integration listed in configuration. ",[14,720,721],{},"contextlib.ExitStack"," enters any number of context managers in a loop and exits them all, in reverse order, when its own block ends.",[72,724,726],{"className":74,"code":725,"language":76,"meta":77,"style":77},"import contextlib\nfrom unittest.mock import patch\n\n\ndef test_every_integration_can_be_disabled(integrations):\n    with contextlib.ExitStack() as stack:\n        mocks = {\n            name: stack.enter_context(patch(target, autospec=True))\n            for name, target in integrations.items()\n        }\n        run_nightly_job()\n\n    for name, mock in mocks.items():\n        mock.assert_not_called(), name\n",[14,727,728,733,737,741,745,750,755,760,765,770,775,780,784,789],{"__ignoreMap":77},[81,729,730],{"class":83,"line":84},[81,731,732],{},"import contextlib\n",[81,734,735],{"class":83,"line":90},[81,736,93],{},[81,738,739],{"class":83,"line":96},[81,740,100],{"emptyLinePlaceholder":99},[81,742,743],{"class":83,"line":103},[81,744,100],{"emptyLinePlaceholder":99},[81,746,747],{"class":83,"line":108},[81,748,749],{},"def test_every_integration_can_be_disabled(integrations):\n",[81,751,752],{"class":83,"line":114},[81,753,754],{},"    with contextlib.ExitStack() as stack:\n",[81,756,757],{"class":83,"line":120},[81,758,759],{},"        mocks = {\n",[81,761,762],{"class":83,"line":126},[81,763,764],{},"            name: stack.enter_context(patch(target, autospec=True))\n",[81,766,767],{"class":83,"line":132},[81,768,769],{},"            for name, target in integrations.items()\n",[81,771,772],{"class":83,"line":185},[81,773,774],{},"        }\n",[81,776,777],{"class":83,"line":191},[81,778,779],{},"        run_nightly_job()\n",[81,781,782],{"class":83,"line":197},[81,783,100],{"emptyLinePlaceholder":99},[81,785,786],{"class":83,"line":202},[81,787,788],{},"    for name, mock in mocks.items():\n",[81,790,791],{"class":83,"line":208},[81,792,793],{},"        mock.assert_not_called(), name\n",[10,795,796,797,799,800,802],{},"The dictionary keyed by name keeps the same property as the named ",[14,798,408],{}," statement: every mock is retrieved by what it replaces, never by position. ",[14,801,31],{}," also guarantees that if entering the fourth patch fails — a typo in its target — the three already entered are unwound, so a broken test cannot leave global state patched for the rest of the session.",[41,804,806],{"id":805},"when-the-patch-list-is-the-bug","When the patch list is the bug",[10,808,809],{},"The mechanics above solve the ordering problem. They do not solve the underlying one, which is that a test needing six patches is testing code that reaches out to six collaborators on its own. Every patch is a dependency the code acquires by import rather than receiving from its caller, and every one is a place where the test has to know an implementation detail — the module path where the code looks the collaborator up.",[10,811,812],{},"The durable fix is to invert those dependencies. A function that takes its repository, clock and mailer as arguments needs no patches at all: the test passes doubles directly, the signature documents what the code depends on, and a refactor that moves the mailer to a different module breaks no tests. Converting one heavily patched function this way is usually a small change, and it tends to reveal that several of the patches were covering the same few collaborators under different names.",[265,814,816,878],{"className":815},[268],[270,817,278,821,278,824,278,827,278,829,278,832,278,837,278,842,278,846,278,850,278,854,278,858,278,861,278,865,278,869,278,872,278,875],{"viewBox":626,"role":273,"ariaLabelledBy":818,"xmlns":277},[819,820],"inv-t","inv-d",[280,822,823],{"id":819},"From many patches to injected collaborators",[284,825,826],{"id":820},"On the left, a function imports six collaborators and its test needs six patches, each coupled to a module path. On the right, the same function receives its collaborators as parameters, the test passes doubles directly with no patching, and the signature documents every dependency.",[306,828],{"x":308,"y":308,"width":646,"height":647,"rx":311,"fill":312},[314,830,831],{"x":650,"y":317,"textAnchor":318,"fontSize":651,"fontWeight":320,"fill":321},"Six patches are six hidden dependencies",[306,833],{"x":325,"y":834,"width":835,"height":836,"rx":664,"fill":382,"stroke":304,"strokeWidth":659},"50","360","164",[314,838,841],{"x":839,"y":375,"textAnchor":318,"fontSize":840,"fontWeight":320,"fill":321},"206","12.5","reaches for collaborators",[314,843,845],{"x":844,"y":339,"fontSize":669,"fill":321},"44","imports repo, clock, mailer, sms,",[314,847,849],{"x":844,"y":848,"fontSize":669,"fill":321},"126","audit, metrics at module level",[314,851,853],{"x":844,"y":836,"fontSize":669,"fontWeight":320,"fill":852},"#8f3d22","test: six patches by module path",[314,855,857],{"x":844,"y":856,"fontSize":669,"fill":321},"186","any move breaks every test",[306,859],{"x":860,"y":834,"width":835,"height":836,"rx":664,"fill":658,"stroke":643,"strokeWidth":659},"414",[314,862,864],{"x":863,"y":375,"textAnchor":318,"fontSize":840,"fontWeight":320,"fill":321},"594","receives collaborators",[314,866,868],{"x":867,"y":339,"fontSize":669,"fill":321},"432","confirm(order_id, *, repo, clock,",[314,870,871],{"x":867,"y":848,"fontSize":669,"fill":321},"notifier)",[314,873,874],{"x":867,"y":836,"fontSize":669,"fontWeight":320,"fill":670},"test: pass doubles, no patches",[314,876,877],{"x":867,"y":856,"fontSize":669,"fill":321},"signature documents dependencies",[403,879,880],{},"The right-hand version often has fewer dependencies too, because injecting them makes it obvious which ones overlap.",[10,882,883,884,888],{},"A useful threshold: three patches in a test is normal, four is worth a second look, and five or more is a design conversation. The approach to having it is laid out in ",[62,885,887],{"href":886},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002F","dependency injection for testability",", and the payoff is tests that stop breaking when files move. It is the rare refactor that makes both the production code and its tests shorter at the same time. Start with the test that has the most patches; it is usually the one that breaks most often.",[41,890,892],{"id":891},"frequently-asked-questions","Frequently Asked Questions",[10,894,895,898,899,902,903,906,907,910],{},[456,896,897],{},"In what order are stacked @patch decorators passed to the test?","\nBottom-up. The decorator closest to the function is applied first and supplies the first mock argument. So ",[14,900,901],{},"@patch(\"a\")"," above ",[14,904,905],{},"@patch(\"b\")"," gives ",[14,908,909],{},"def test(mock_b, mock_a)",". Getting this backwards silently configures the wrong mock.",[10,912,913,916,917,919,920,922,923,925],{},[456,914,915],{},"What is the cleanest way to apply several patches in one test?","\nA parenthesised ",[14,918,408],{}," statement (Python 3.10+) naming each mock with ",[14,921,445],{},", or ",[14,924,429],{}," for several attributes on one object. Both keep each patch next to its name, so the order cannot be confused.",[10,927,928,931],{},[456,929,930],{},"How many patches is too many?","\nWhen a test needs more than three or four, the code under test usually has too many hard-wired collaborators. Each patch is a dependency the code reaches for rather than receives. Injecting them removes the patches entirely and makes the dependencies visible in the signature.",[41,933,935],{"id":934},"related","Related",[46,937,938,945,952,959],{},[49,939,940,944],{},[62,941,943],{"href":942},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002F","Patching Strategies for Complex Codebases"," — the wider patching model.",[49,946,947,951],{},[62,948,950],{"href":949},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-class-attributes-with-patch-object\u002F","Patching Class Attributes with patch.object"," — object-based patches that combine cleanly.",[49,953,954,958],{},[62,955,957],{"href":956},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Fwiring-test-doubles-through-a-factory-function\u002F","Wiring Test Doubles Through a Factory Function"," — the alternative to patching many collaborators.",[49,960,961,965],{},[62,962,964],{"href":963},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Ftaming-autouse-fixtures-in-large-suites\u002F","Taming Autouse Fixtures in Large Suites"," — when a patch fixture should and should not be autouse.",[10,967,968,969],{},"← Back to ",[62,970,943],{"href":942},[972,973,974],"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":77,"searchDepth":90,"depth":90,"links":976},[977,978,979,980,981,982,983,984,985],{"id":43,"depth":90,"text":44},{"id":69,"depth":90,"text":70},{"id":412,"depth":90,"text":413},{"id":449,"depth":90,"text":450},{"id":502,"depth":90,"text":503},{"id":714,"depth":90,"text":715},{"id":805,"depth":90,"text":806},{"id":891,"depth":90,"text":892},{"id":934,"depth":90,"text":935},"Combine several patches without mixing up mock arguments: bottom-up decorator order, parenthesised with-statements, ExitStack, fixtures, and when too many patches is the real bug.","md",{"slug":989,"type":990,"breadcrumb":991,"datePublished":992,"dateModified":992,"faq":993,"howto":1000},"stacking-multiple-patches-without-argument-confusion","article","Stacking Patches","2026-09-18",[994,996,998],{"q":897,"a":995},"Bottom-up. The decorator closest to the function is applied first and supplies the first mock argument. So @patch('a') above @patch('b') gives def test(mock_b, mock_a). Getting this backwards silently configures the wrong mock.",{"q":915,"a":997},"A parenthesised with-statement (Python 3.10+) naming each mock with 'as', or patch.multiple for several attributes on one object. Both keep each patch next to its name, so the order cannot be confused.",{"q":930,"a":999},"When a test needs more than three or four, the code under test usually has too many hard-wired collaborators. Each patch is a dependency the code reaches for rather than receives. Injecting them removes the patches entirely and makes the dependencies visible in the signature.",{"name":1001,"description":1002,"steps":1003},"How to apply several patches without mixing them up","Prefer named context managers over stacked decorators, group related patches, and treat a long patch list as a design signal.",[1004,1007,1010,1013,1016],{"name":1005,"text":1006},"Prefer a parenthesised with-statement","Name each patch with as so the binding sits next to its target.",{"name":1008,"text":1009},"Use patch.multiple for one object","Patch several attributes of the same module or class in a single call.",{"name":1011,"text":1012},"Move shared patches into fixtures","Give each commonly patched collaborator a named fixture so tests request it by name.",{"name":1014,"text":1015},"Use ExitStack for dynamic sets","Enter a variable number of patches in a loop with contextlib.ExitStack.",{"name":1017,"text":1018},"Count the patches","Treat more than three or four as a sign the code should receive its collaborators instead.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fstacking-multiple-patches-without-argument-confusion",{"title":5,"description":986},"advanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fstacking-multiple-patches-without-argument-confusion\u002Findex","gbt29aRceXdwlTw_d5mmF6-n2o3gGnk3rwn_5KAlhzI",1789718767612]