[{"data":1,"prerenderedAt":932},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002Fdriving-mocks-with-side-effect-sequences\u002F":3},{"id":4,"title":5,"body":6,"description":895,"extension":896,"meta":897,"navigation":87,"path":928,"seo":929,"stem":930,"__hash__":931},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002Fdriving-mocks-with-side-effect-sequences\u002Findex.md","Driving Mocks with side_effect Sequences",{"type":7,"value":8,"toc":885},"minimark",[9,21,31,36,61,65,293,446,450,470,476,488,492,558,562,565,585,594,600,609,694,708,712,718,724,727,810,814,827,837,843,847,876,881],[10,11,12,16,17,20],"p",{},[13,14,15],"code",{},"return_value"," gives a mock one answer for every call. Real collaborators are rarely that consistent: a flaky service fails twice then succeeds, a paginated API returns three pages then an empty one, a cache misses on the first lookup and hits on the second. ",[13,18,19],{},"side_effect"," accepts an iterable, and each call consumes the next item — returning it, or raising it if it is an exception. That single feature turns a mock from a constant into a script, and it is the standard way to test retry loops, pagination, and state-dependent behaviour without building a fake.",[10,22,23,24,27,28,30],{},"The feature has one sharp edge worth knowing before relying on it: when the iterable runs out, the next call raises ",[13,25,26],{},"StopIteration",", which in generator and coroutine contexts can turn into something much stranger than a test failure. Sizing the sequence deliberately, and asserting it was consumed exactly, keeps that edge from ever cutting. This guide covers both forms of ",[13,29,19],{}," — iterables for behaviour that depends on call order and functions for behaviour that depends on arguments — along with the recurring scripts worth naming, and the point at which a script should give way to a fake.",[32,33,35],"h2",{"id":34},"prerequisites","Prerequisites",[37,38,39,47,53],"ul",{},[40,41,42,43,46],"li",{},"Python 3.8+; ",[13,44,45],{},"unittest.mock"," in the standard library.",[40,48,49,52],{},[13,50,51],{},"pytest >= 8.0",".",[40,54,55,56,52],{},"The basics of mock configuration from ",[57,58,60],"a",{"href":59},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002F","deep dive into unittest.mock",[32,62,64],{"id":63},"solution","Solution",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-python shiki shiki-themes github-light github-dark","from unittest.mock import Mock\n\nimport pytest\n\n\ndef test_retry_succeeds_on_the_third_attempt():\n    fetch = Mock(side_effect=[\n        TimeoutError(\"attempt 1\"),            # raised\n        TimeoutError(\"attempt 2\"),            # raised\n        {\"status\": \"ok\"},                     # returned\n    ])\n\n    result = fetch_with_retry(fetch, retries=3)\n\n    assert result == {\"status\": \"ok\"}\n    assert fetch.call_count == 3              # the whole script was used\n\n\ndef test_retry_gives_up_after_the_limit():\n    fetch = Mock(side_effect=[TimeoutError] * 3)    # classes are raised too\n\n    with pytest.raises(TimeoutError):\n        fetch_with_retry(fetch, retries=3)\n\n    assert fetch.call_count == 3              # and not a fourth time\n\n\ndef test_lookup_depends_on_the_argument():\n    users = {\"u1\": {\"name\": \"Ada\"}, \"u2\": {\"name\": \"Alan\"}}\n\n    def lookup(user_id):\n        # A function: behaviour driven by arguments, not call order.\n        if user_id not in users:\n            raise KeyError(user_id)\n        return users[user_id]\n\n    repo = Mock(get=Mock(side_effect=lookup))\n    assert greet(repo, \"u2\") == \"Hello, Alan\"\n","python","",[13,73,74,82,89,95,100,105,111,117,123,129,135,141,146,152,157,163,169,174,179,185,191,196,202,208,213,219,224,229,235,241,246,252,258,264,270,276,281,287],{"__ignoreMap":71},[75,76,79],"span",{"class":77,"line":78},"line",1,[75,80,81],{},"from unittest.mock import Mock\n",[75,83,85],{"class":77,"line":84},2,[75,86,88],{"emptyLinePlaceholder":87},true,"\n",[75,90,92],{"class":77,"line":91},3,[75,93,94],{},"import pytest\n",[75,96,98],{"class":77,"line":97},4,[75,99,88],{"emptyLinePlaceholder":87},[75,101,103],{"class":77,"line":102},5,[75,104,88],{"emptyLinePlaceholder":87},[75,106,108],{"class":77,"line":107},6,[75,109,110],{},"def test_retry_succeeds_on_the_third_attempt():\n",[75,112,114],{"class":77,"line":113},7,[75,115,116],{},"    fetch = Mock(side_effect=[\n",[75,118,120],{"class":77,"line":119},8,[75,121,122],{},"        TimeoutError(\"attempt 1\"),            # raised\n",[75,124,126],{"class":77,"line":125},9,[75,127,128],{},"        TimeoutError(\"attempt 2\"),            # raised\n",[75,130,132],{"class":77,"line":131},10,[75,133,134],{},"        {\"status\": \"ok\"},                     # returned\n",[75,136,138],{"class":77,"line":137},11,[75,139,140],{},"    ])\n",[75,142,144],{"class":77,"line":143},12,[75,145,88],{"emptyLinePlaceholder":87},[75,147,149],{"class":77,"line":148},13,[75,150,151],{},"    result = fetch_with_retry(fetch, retries=3)\n",[75,153,155],{"class":77,"line":154},14,[75,156,88],{"emptyLinePlaceholder":87},[75,158,160],{"class":77,"line":159},15,[75,161,162],{},"    assert result == {\"status\": \"ok\"}\n",[75,164,166],{"class":77,"line":165},16,[75,167,168],{},"    assert fetch.call_count == 3              # the whole script was used\n",[75,170,172],{"class":77,"line":171},17,[75,173,88],{"emptyLinePlaceholder":87},[75,175,177],{"class":77,"line":176},18,[75,178,88],{"emptyLinePlaceholder":87},[75,180,182],{"class":77,"line":181},19,[75,183,184],{},"def test_retry_gives_up_after_the_limit():\n",[75,186,188],{"class":77,"line":187},20,[75,189,190],{},"    fetch = Mock(side_effect=[TimeoutError] * 3)    # classes are raised too\n",[75,192,194],{"class":77,"line":193},21,[75,195,88],{"emptyLinePlaceholder":87},[75,197,199],{"class":77,"line":198},22,[75,200,201],{},"    with pytest.raises(TimeoutError):\n",[75,203,205],{"class":77,"line":204},23,[75,206,207],{},"        fetch_with_retry(fetch, retries=3)\n",[75,209,211],{"class":77,"line":210},24,[75,212,88],{"emptyLinePlaceholder":87},[75,214,216],{"class":77,"line":215},25,[75,217,218],{},"    assert fetch.call_count == 3              # and not a fourth time\n",[75,220,222],{"class":77,"line":221},26,[75,223,88],{"emptyLinePlaceholder":87},[75,225,227],{"class":77,"line":226},27,[75,228,88],{"emptyLinePlaceholder":87},[75,230,232],{"class":77,"line":231},28,[75,233,234],{},"def test_lookup_depends_on_the_argument():\n",[75,236,238],{"class":77,"line":237},29,[75,239,240],{},"    users = {\"u1\": {\"name\": \"Ada\"}, \"u2\": {\"name\": \"Alan\"}}\n",[75,242,244],{"class":77,"line":243},30,[75,245,88],{"emptyLinePlaceholder":87},[75,247,249],{"class":77,"line":248},31,[75,250,251],{},"    def lookup(user_id):\n",[75,253,255],{"class":77,"line":254},32,[75,256,257],{},"        # A function: behaviour driven by arguments, not call order.\n",[75,259,261],{"class":77,"line":260},33,[75,262,263],{},"        if user_id not in users:\n",[75,265,267],{"class":77,"line":266},34,[75,268,269],{},"            raise KeyError(user_id)\n",[75,271,273],{"class":77,"line":272},35,[75,274,275],{},"        return users[user_id]\n",[75,277,279],{"class":77,"line":278},36,[75,280,88],{"emptyLinePlaceholder":87},[75,282,284],{"class":77,"line":283},37,[75,285,286],{},"    repo = Mock(get=Mock(side_effect=lookup))\n",[75,288,290],{"class":77,"line":289},38,[75,291,292],{},"    assert greet(repo, \"u2\") == \"Hello, Alan\"\n",[294,295,298,442],"figure",{"className":296},[297],"diagram",[299,300,307,308,307,312,307,316,307,334,307,342,307,351,307,360,307,366,307,372,307,375,307,378,307,381,307,386,307,390,307,394,307,401,307,405,307,408,307,415,307,419,307,423,307,429,307,434,307,438],"svg",{"viewBox":301,"role":302,"ariaLabelledBy":303,"xmlns":306},"0 0 820 262","img",[304,305],"se-t","se-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[309,310,311],"title",{"id":304},"A side_effect sequence consumed call by call",[313,314,315],"desc",{"id":305},"A list of three items: TimeoutError, TimeoutError and a success dictionary. The first call consumes the first item and raises it, the second call raises the second, and the third call returns the dictionary. A fourth call would find the list exhausted and raise StopIteration.",[317,318,319,320,307],"defs",{},"\n    ",[321,322,329],"marker",{"id":323,"viewBox":324,"refX":325,"refY":326,"markerWidth":327,"markerHeight":327,"orient":328},"se-a","0 0 10 10","9","5","7","auto-start-reverse",[330,331],"path",{"d":332,"fill":333},"M0 0 L10 5 L0 10 z","#3d405b",[335,336],"rect",{"x":337,"y":337,"width":338,"height":339,"rx":340,"fill":341},"0","820","262","14","#fffdf8",[343,344,350],"text",{"x":345,"y":346,"textAnchor":347,"fontSize":348,"fontWeight":349,"fill":333},"410","28","middle","16","700","Each call takes the next item off the script",[335,352],{"x":353,"y":354,"width":355,"height":354,"rx":356,"fill":357,"stroke":358,"strokeWidth":359},"40","60","170","10","#fbe9e3","#e07a5f","2",[343,361,365],{"x":362,"y":363,"textAnchor":347,"fontSize":364,"fontWeight":349,"fill":333},"125","86","11.5","TimeoutError",[343,367,371],{"x":362,"y":368,"textAnchor":347,"fontSize":369,"fill":370},"106","11","#8f3d22","call 1 · raised",[335,373],{"x":374,"y":354,"width":355,"height":354,"rx":356,"fill":357,"stroke":358,"strokeWidth":359},"230",[343,376,365],{"x":377,"y":363,"textAnchor":347,"fontSize":364,"fontWeight":349,"fill":333},"315",[343,379,380],{"x":377,"y":368,"textAnchor":347,"fontSize":369,"fill":370},"call 2 · raised",[335,382],{"x":383,"y":354,"width":355,"height":354,"rx":356,"fill":384,"stroke":385,"strokeWidth":359},"420","#e6f0ea","#81b29a",[343,387,389],{"x":388,"y":363,"textAnchor":347,"fontSize":364,"fontWeight":349,"fill":333},"505","{\"status\": \"ok\"}",[343,391,393],{"x":388,"y":368,"textAnchor":347,"fontSize":369,"fill":392},"#2a5f49","call 3 · returned",[335,395],{"x":396,"y":354,"width":355,"height":354,"rx":356,"fill":341,"stroke":397,"strokeWidth":398,"strokeDashArray":399},"610","rgba(61,64,91,0.35)","1.6",[326,400],"4",[343,402,404],{"x":403,"y":363,"textAnchor":347,"fontSize":364,"fontWeight":349,"fill":333},"695","exhausted",[343,406,407],{"x":403,"y":368,"textAnchor":347,"fontSize":369,"fill":333},"call 4 · StopIteration",[77,409],{"x1":410,"y1":411,"x2":412,"y2":411,"stroke":333,"strokeWidth":413,"markerEnd":414},"214","90","226","1.5","url(#se-a)",[77,416],{"x1":417,"y1":411,"x2":418,"y2":411,"stroke":333,"strokeWidth":413,"markerEnd":414},"404","416",[77,420],{"x1":421,"y1":411,"x2":422,"y2":411,"stroke":333,"strokeWidth":413,"markerEnd":414},"594","606",[335,424],{"x":353,"y":425,"width":426,"height":427,"rx":369,"fill":428,"stroke":333,"strokeWidth":398},"150","740","88","#f4f1de",[343,430,433],{"x":345,"y":431,"textAnchor":347,"fontSize":432,"fontWeight":349,"fill":333},"176","12","assert fetch.call_count == 3",[343,435,437],{"x":345,"y":436,"textAnchor":347,"fontSize":369,"fill":333},"198","catches a fourth call (over-retrying) and a second call only (giving up too early)",[343,439,441],{"x":345,"y":440,"textAnchor":347,"fontSize":369,"fill":333},"218","— the two ways retry logic is usually wrong",[443,444,445],"figcaption",{},"The script defines what the collaborator does; the call-count assertion defines how many times the code under test was allowed to ask.",[32,447,449],{"id":448},"why-this-works","Why this works",[10,451,452,453,455,456,459,460,463,464,466,467,469],{},"When ",[13,454,19],{}," is an iterable, ",[13,457,458],{},"Mock"," converts it to an iterator at assignment and calls ",[13,461,462],{},"next()"," on it for each invocation. If the item is an exception class or instance, the mock raises it; otherwise it returns it. The mock's ",[13,465,15],{}," is ignored while ",[13,468,19],{}," is set, so the script fully determines behaviour.",[10,471,472,473,475],{},"Because the iterator is created once at assignment, the script is stateful across the whole test: calls made during setup consume items just as calls made during the action do. That is usually what is wanted, and occasionally the cause of a confusing failure when a fixture happens to call the mock once before the test body runs. Assigning ",[13,474,19],{}," inside the test, immediately before the action, avoids the surprise.",[10,477,452,478,480,481,484,485,487],{},[13,479,19],{}," is a callable, the mock calls it with the same arguments it received and returns the callable's result — unless the callable returns the special ",[13,482,483],{},"mock.DEFAULT"," sentinel, in which case the mock falls back to its ",[13,486,15],{},". That lets a function handle a few special cases and defer everything else to a configured default. It is a small feature that keeps function-valued side effects short: the function only has to describe the interesting inputs, and every ordinary call falls through to the same default the rest of the test already relies on.",[32,489,491],{"id":490},"edge-cases-and-failure-modes","Edge cases and failure modes",[37,493,494,511,517,523,537],{},[40,495,496,500,501,503,504,507,508,52],{},[497,498,499],"strong",{},"Running out."," A fourth call against a three-item script raises ",[13,502,26],{},". In a generator, Python converts that to ",[13,505,506],{},"RuntimeError: generator raised StopIteration",", which looks nothing like a mock problem. Size scripts exactly and assert ",[13,509,510],{},"call_count",[40,512,513,516],{},[497,514,515],{},"Mutable items."," The same dictionary returned from two positions is the same object. If the code mutates it, the second call returns the mutated version. Use separate literals.",[40,518,519,522],{},[497,520,521],{},"Exception instances reused."," Raising the same exception instance twice accumulates a traceback. Use classes, or separate instances.",[40,524,525,533,534,536],{},[497,526,527,529,530,532],{},[13,528,19],{}," and ",[13,531,15],{}," together."," ",[13,535,19],{}," wins. Setting both is usually a sign one of them is left over from an earlier version of the test.",[40,538,539,542,543,546,547,551,552,554,555,52],{},[497,540,541],{},"Async code."," For ",[13,544,545],{},"AsyncMock",", each item is the result of the ",[548,549,550],"em",{},"await",". Exceptions are raised on await, not on call, which matters when asserting on ",[13,553,510],{}," versus ",[13,556,557],{},"await_count",[32,559,561],{"id":560},"common-scripts-worth-recognising","Common scripts worth recognising",[10,563,564],{},"A handful of scripts recur across codebases, and recognising them makes the corresponding tests quick to write and easy to read.",[10,566,567,570,571,574,575,570,578,581,582,584],{},[497,568,569],{},"Transient failure, then success"," — ",[13,572,573],{},"[ConnectionError, ConnectionError, response]"," — is the retry test. Its partner, ",[497,576,577],{},"permanent failure",[13,579,580],{},"[ConnectionError] * 3"," with a retry limit of three — proves the loop gives up. Together they bound the retry behaviour from both sides, and both assert on ",[13,583,510],{}," so neither over- nor under-retrying slips through.",[10,586,587,570,590,593],{},[497,588,589],{},"Pagination",[13,591,592],{},"[page_1, page_2, empty_page]"," — tests that the consumer follows the sequence until the empty page and stops. The empty page matters as much as the full ones: without it the test cannot tell whether the consumer stopped because it saw the end or because the script ran out.",[10,595,596,599],{},[497,597,598],{},"Cache miss, then hit"," — a spy whose underlying fetch returns once — belongs with spies rather than scripts, since the point is to observe that the second call never reached the backing store.",[10,601,602,570,605,608],{},[497,603,604],{},"Rate limiting",[13,606,607],{},"[RateLimited(retry_after=2), response]"," — tests that the code honours a retry-after hint, ideally combined with a fake clock so the test asserts the delay requested rather than actually waiting for it.",[294,610,612,691],{"className":611},[297],[299,613,307,618,307,621,307,624,307,628,307,633,307,639,307,644,307,648,307,652,307,655,307,659,307,661,307,664,307,669,307,672,307,676,307,680,307,682,307,685,307,688],{"viewBox":614,"role":302,"ariaLabelledBy":615,"xmlns":306},"0 0 800 244",[616,617],"pat-t","pat-d",[309,619,620],{"id":616},"Four recurring side_effect scripts",[313,622,623],{"id":617},"Four named scripts. Transient failure then success tests that retries recover. Permanent failure tests that retries stop at the limit. Pagination ending in an empty page tests that the consumer stops at the end. Rate limiting with a retry-after hint tests that the requested delay is honoured, ideally with a fake clock.",[335,625],{"x":337,"y":337,"width":626,"height":627,"rx":340,"fill":341},"800","244",[343,629,632],{"x":630,"y":346,"textAnchor":347,"fontSize":631,"fontWeight":349,"fill":333},"400","15.5","Name the scenario, not the list",[335,634],{"x":635,"y":636,"width":637,"height":638,"rx":369,"fill":384,"stroke":385,"strokeWidth":359},"24","50","370","84",[343,640,643],{"x":641,"y":642,"fontSize":432,"fontWeight":349,"fill":333},"44","76","transient_failure(times=2, then=ok)",[343,645,647],{"x":641,"y":646,"fontSize":369,"fill":333},"100","[ConnectionError, ConnectionError, ok]",[343,649,651],{"x":641,"y":650,"fontSize":369,"fill":392},"120","retries recover",[335,653],{"x":654,"y":636,"width":637,"height":638,"rx":369,"fill":357,"stroke":358,"strokeWidth":359},"406",[343,656,658],{"x":657,"y":642,"fontSize":432,"fontWeight":349,"fill":333},"426","permanent_failure(times=3)",[343,660,580],{"x":657,"y":646,"fontSize":369,"fill":333},[343,662,663],{"x":657,"y":650,"fontSize":369,"fill":370},"retries stop at the limit",[335,665],{"x":635,"y":666,"width":637,"height":638,"rx":369,"fill":667,"stroke":668,"strokeWidth":359},"144","#f7f0da","#f2cc8f",[343,670,671],{"x":641,"y":355,"fontSize":432,"fontWeight":349,"fill":333},"pages(p1, p2)",[343,673,675],{"x":641,"y":674,"fontSize":369,"fill":333},"194","[p1, p2, empty]",[343,677,679],{"x":641,"y":410,"fontSize":369,"fill":678},"#8a5a00","the empty page is the point",[335,681],{"x":654,"y":666,"width":637,"height":638,"rx":369,"fill":428,"stroke":333,"strokeWidth":398},[343,683,684],{"x":657,"y":355,"fontSize":432,"fontWeight":349,"fill":333},"rate_limited(retry_after=2, then=ok)",[343,686,687],{"x":657,"y":674,"fontSize":369,"fill":333},"[RateLimited(2), ok]",[343,689,690],{"x":657,"y":410,"fontSize":369,"fill":333},"assert the delay, do not wait for it",[443,692,693],{},"Each helper encodes the one detail that is easy to get wrong in a hand-written list, and reads as a sentence at the call site.",[10,695,696,697,700,701,704,705,707],{},"Naming these patterns in a shared test-support module — ",[13,698,699],{},"transient_failure(times=2, then=response)",", ",[13,702,703],{},"pages(*items)"," — turns a script from a bare list into a statement of the scenario. It also centralises the one detail that is easy to get wrong in each: the trailing empty page, the exact retry count, the retry-after value. A reader seeing ",[13,706,643],{}," understands the test's premise immediately, which is the whole reason for scripting the mock rather than faking the service.",[32,709,711],{"id":710},"scripts-versus-fakes","Scripts versus fakes",[10,713,714,715,717],{},"A ",[13,716,19],{}," script is excellent for a collaborator whose behaviour in the test is a short, fixed sequence: three attempts, two pages, one miss then one hit. It becomes a liability when the sequence gets long or when it has to agree with other parts of the test. A twelve-item script modelling a paginated API with a cursor, where the items must match the cursor values the code sends, is really a fake written as a list — hard to read, easy to get subtly inconsistent, and impossible to reuse.",[10,719,720,721,723],{},"The signal to switch is when the script starts needing to know about its inputs. A function-valued ",[13,722,19],{}," covers the middle ground — behaviour depending on arguments, without a class — and a small fake covers the rest. The paginated case, for example, becomes a function that slices a list of records by the cursor it receives, which is shorter than the script, correct for any page size, and trivially reused by every test that paginates.",[10,725,726],{},"The rule of thumb that works well in practice: if the script has more than about five items, or if any item's correctness depends on an argument the code passes, write a function or a fake instead. The script's virtue is that it makes the scenario readable at a glance, and that virtue disappears once reading it requires cross-referencing the calls. Moving to a function or a fake at that point is not a failure of the technique but the natural next step, and it is usually a shorter change than expected because the scenario was already well understood from writing the script.",[294,728,730,807],{"className":729},[297],[299,731,307,736,307,739,307,742,307,745,307,748,307,752,307,755,307,759,307,763,307,767,307,770,307,773,307,775,307,778,307,782,307,785,307,788,307,791,307,793,307,797,307,801,307,804],{"viewBox":732,"role":302,"ariaLabelledBy":733,"xmlns":306},"0 0 800 236",[734,735],"sf-t","sf-d",[309,737,738],{"id":734},"Choosing between a script, a function and a fake",[313,740,741],{"id":735},"Three options by complexity. A short list suits fixed sequences of up to about five outcomes that do not depend on arguments. A function-valued side effect suits behaviour that depends on arguments. A fake class suits long or stateful behaviour, such as pagination with cursors, that many tests need.",[335,743],{"x":337,"y":337,"width":626,"height":744,"rx":340,"fill":341},"236",[343,746,747],{"x":630,"y":346,"textAnchor":347,"fontSize":631,"fontWeight":349,"fill":333},"More dependence on inputs, more structure needed",[335,749],{"x":635,"y":636,"width":750,"height":751,"rx":432,"fill":384,"stroke":385,"strokeWidth":359},"240","164",[335,753],{"x":635,"y":636,"width":750,"height":754,"rx":432,"fill":333},"30",[343,756,758],{"x":666,"y":757,"textAnchor":347,"fontSize":432,"fontWeight":349,"fill":341},"70","list script",[343,760,762],{"x":353,"y":761,"fontSize":369,"fill":333},"104","fixed sequence",[343,764,766],{"x":353,"y":765,"fontSize":369,"fill":333},"126","≤ 5 outcomes",[343,768,769],{"x":353,"y":355,"fontSize":369,"fill":392},"retries, one miss then hit",[335,771],{"x":772,"y":636,"width":750,"height":751,"rx":432,"fill":667,"stroke":668,"strokeWidth":359},"280",[335,774],{"x":772,"y":636,"width":750,"height":754,"rx":432,"fill":333},[343,776,777],{"x":630,"y":757,"textAnchor":347,"fontSize":432,"fontWeight":349,"fill":341},"function",[343,779,781],{"x":780,"y":761,"fontSize":369,"fill":333},"296","result depends on args",[343,783,784],{"x":780,"y":765,"fontSize":369,"fill":333},"no persistent state",[343,786,787],{"x":780,"y":355,"fontSize":369,"fill":678},"lookups by id, conditional errors",[335,789],{"x":790,"y":636,"width":750,"height":751,"rx":432,"fill":357,"stroke":358,"strokeWidth":359},"536",[335,792],{"x":790,"y":636,"width":750,"height":754,"rx":432,"fill":333},[343,794,796],{"x":795,"y":757,"textAnchor":347,"fontSize":432,"fontWeight":349,"fill":341},"656","fake class",[343,798,800],{"x":799,"y":761,"fontSize":369,"fill":333},"552","stateful, long, reused",[343,802,803],{"x":799,"y":765,"fontSize":369,"fill":333},"many tests need it",[343,805,806],{"x":799,"y":355,"fontSize":369,"fill":370},"pagination, caches, stores",[443,808,809],{},"Moving right costs a little more code and buys correctness that no longer depends on the test author keeping a long list consistent by hand.",[32,811,813],{"id":812},"frequently-asked-questions","Frequently Asked Questions",[10,815,816,819,820,822,823,826],{},[497,817,818],{},"What happens when a side_effect iterable runs out?","\nThe next call raises ",[13,821,26],{},". In synchronous code that surfaces as a confusing error from inside the mock; in a generator or coroutine it can be converted into a ",[13,824,825],{},"RuntimeError"," or silently end iteration. Size the sequence to the expected number of calls, and treat running out as a test failure.",[10,828,829,832,833,836],{},[497,830,831],{},"Can a side_effect sequence mix values and exceptions?","\nYes. Each item is either returned or, if it is an exception class or instance, raised. ",[13,834,835],{},"[TimeoutError, TimeoutError, {\"ok\": True}]"," raises twice and then returns the dictionary, which is the canonical way to test retry logic.",[10,838,839,842],{},[497,840,841],{},"When should side_effect be a function rather than a list?","\nWhen the result depends on the arguments rather than on the call count — returning different users for different ids, or raising only for a particular input. A function receives the same arguments as the mock and its return value becomes the mock's.",[32,844,846],{"id":845},"related","Related",[37,848,849,855,862,869],{},[40,850,851,854],{},[57,852,853],{"href":59},"Deep Dive into unittest.mock"," — how mocks are configured and what they record.",[40,856,857,861],{},[57,858,860],{"href":859},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fresolving-side_effect-and-return_value-conflicts\u002F","Resolving side_effect and return_value Conflicts"," — the precedence rules between the two.",[40,863,864,868],{},[57,865,867],{"href":866},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Ftesting-retry-and-backoff-logic-without-waiting\u002F","Testing Retry and Backoff Logic Without Waiting"," — scripts plus a fake clock.",[40,870,871,875],{},[57,872,874],{"href":873},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002Fwriting-an-in-memory-fake-repository\u002F","Writing an In-Memory Fake Repository"," — where to go when the script grows too long.",[10,877,878,879],{},"← Back to ",[57,880,853],{"href":59},[882,883,884],"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":71,"searchDepth":84,"depth":84,"links":886},[887,888,889,890,891,892,893,894],{"id":34,"depth":84,"text":35},{"id":63,"depth":84,"text":64},{"id":448,"depth":84,"text":449},{"id":490,"depth":84,"text":491},{"id":560,"depth":84,"text":561},{"id":710,"depth":84,"text":711},{"id":812,"depth":84,"text":813},{"id":845,"depth":84,"text":846},"Script a mock's behaviour across calls with side_effect iterables: mixed return values and exceptions, StopIteration pitfalls, callables for argument-dependent results, and reset.","md",{"slug":898,"type":899,"breadcrumb":900,"datePublished":901,"dateModified":901,"faq":902,"howto":909},"driving-mocks-with-side-effect-sequences","article","side_effect Sequences","2026-09-18",[903,905,907],{"q":818,"a":904},"The next call raises StopIteration. In synchronous code that surfaces as a confusing error from inside the mock; in a generator or coroutine it can be converted into a RuntimeError or silently end iteration. Size the sequence to the expected number of calls, and treat running out as a test failure.",{"q":831,"a":906},"Yes. Each item is either returned or, if it is an exception class or instance, raised. [TimeoutError, TimeoutError, {'ok': True}] raises twice and then returns the dictionary, which is the canonical way to test retry logic.",{"q":841,"a":908},"When the result depends on the arguments rather than on the call count — returning different users for different ids, or raising only for a particular input. A function receives the same arguments as the mock and its return value becomes the mock's.",{"name":910,"description":911,"steps":912},"How to script a mock's behaviour with side_effect","Use an iterable for call-count-dependent behaviour, a function for argument-dependent behaviour, and assert that the sequence was consumed exactly.",[913,916,919,922,925],{"name":914,"text":915},"Decide what drives the behaviour","Choose an iterable when results depend on call order and a function when they depend on arguments.",{"name":917,"text":918},"Write the sequence explicitly","List each result in order, using exception classes or instances where a call should raise.",{"name":920,"text":921},"Size it to the expected calls","Provide exactly as many items as the code should make, so an extra call fails loudly.",{"name":923,"text":924},"Assert the sequence was consumed","Check call_count equals the sequence length so under-calling is caught as well as over-calling.",{"name":926,"text":927},"Reset between phases","Call reset_mock(side_effect=True) or reassign side_effect when a test has several phases.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002Fdriving-mocks-with-side-effect-sequences",{"title":5,"description":895},"advanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002Fdriving-mocks-with-side-effect-sequences\u002Findex","FukshJjE3gHQWw-KN6A4pA370cyfhg8HR5hBtKSnxKE",1789718768842]