[{"data":1,"prerenderedAt":1022},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-async-code-and-coroutines\u002Fpatching-an-async-context-manager\u002F":3},{"id":4,"title":5,"body":6,"description":985,"extension":986,"meta":987,"navigation":102,"path":1018,"seo":1019,"stem":1020,"__hash__":1021},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-async-code-and-coroutines\u002Fpatching-an-async-context-manager\u002Findex.md","Patching an Async Context Manager",{"type":7,"value":8,"toc":975},"minimark",[9,29,35,40,73,77,80,236,386,390,416,426,460,464,553,557,569,575,588,662,666,669,787,798,805,859,863,894,909,933,937,966,971],[10,11,12,16,17,20,21,24,25,28],"p",{},[13,14,15],"code",{},"async with pool.acquire() as conn:"," is one of the most common lines in async Python and one of the easiest to mock incorrectly. The manager has two async protocol methods, the variable after ",[13,18,19],{},"as"," is bound to whatever ",[13,22,23],{},"__aenter__"," returns rather than to the manager itself, and ",[13,26,27],{},"__aexit__","'s return value decides whether an exception in the body propagates or vanishes. Get the last one wrong and the test passes while silently swallowing the very error it was written to observe.",[10,30,31,32,34],{},"These mistakes are especially costly because async context managers are almost always guarding a scarce resource — a pooled connection, a lock, a transaction, an open stream — and the whole point of the manager is that the resource is released on every exit path. A mock that suppresses exceptions or hands the body the wrong object makes a test pass precisely in the situation where the real resource would leak. The fix is a few explicit lines rather than reliance on auto-configured mocks, plus an assertion that cleanup actually ran. Once written as a small helper, the pattern is reusable for every pool, session, lock and client in an async codebase, and the helper itself becomes the place where the rule about ",[13,33,27],{}," is encoded once rather than remembered everywhere.",[36,37,39],"h2",{"id":38},"prerequisites","Prerequisites",[41,42,43,55,66],"ul",{},[44,45,46,47,50,51,54],"li",{},"Python 3.8+ for ",[13,48,49],{},"AsyncMock","; 3.10+ is recommended for the improved auto-detection in ",[13,52,53],{},"patch",".",[44,56,57,60,61,54],{},[13,58,59],{},"pytest >= 8.0"," with an async runner, as in ",[62,63,65],"a",{"href":64},"\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002F","pytest-asyncio in depth",[44,67,68,69,54],{},"The async mocking basics from ",[62,70,72],{"href":71},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-async-code-and-coroutines\u002F","patching async code and coroutines",[36,74,76],{"id":75},"solution","Solution",[10,78,79],{},"A helper builds the manager correctly once; tests use it everywhere.",[81,82,87],"pre",{"className":83,"code":84,"language":85,"meta":86,"style":86},"language-python shiki shiki-themes github-light github-dark","from unittest.mock import AsyncMock, MagicMock\n\nimport pytest\n\n\ndef async_cm(value):\n    \"\"\"An async context manager double that behaves like a real one.\"\"\"\n    cm = MagicMock()\n    cm.__aenter__ = AsyncMock(return_value=value)    # what `as` binds to\n    cm.__aexit__ = AsyncMock(return_value=False)     # False: exceptions propagate\n    return cm\n\n\nasync def test_failed_query_still_releases_the_connection(repo):\n    connection = AsyncMock()\n    connection.fetch.side_effect = RuntimeError(\"query failed\")\n    repo.pool.acquire = MagicMock(return_value=async_cm(connection))\n\n    with pytest.raises(RuntimeError, match=\"query failed\"):\n        await repo.load_orders(\"cus_1\")\n\n    manager = repo.pool.acquire.return_value\n    manager.__aexit__.assert_awaited_once()            # cleanup ran on the failure path\n    exc_type, exc, _tb = manager.__aexit__.await_args.args\n    assert exc_type is RuntimeError                    # and saw the real exception\n","python","",[13,88,89,97,104,110,115,120,126,132,138,144,150,156,161,166,172,178,184,190,195,201,207,212,218,224,230],{"__ignoreMap":86},[90,91,94],"span",{"class":92,"line":93},"line",1,[90,95,96],{},"from unittest.mock import AsyncMock, MagicMock\n",[90,98,100],{"class":92,"line":99},2,[90,101,103],{"emptyLinePlaceholder":102},true,"\n",[90,105,107],{"class":92,"line":106},3,[90,108,109],{},"import pytest\n",[90,111,113],{"class":92,"line":112},4,[90,114,103],{"emptyLinePlaceholder":102},[90,116,118],{"class":92,"line":117},5,[90,119,103],{"emptyLinePlaceholder":102},[90,121,123],{"class":92,"line":122},6,[90,124,125],{},"def async_cm(value):\n",[90,127,129],{"class":92,"line":128},7,[90,130,131],{},"    \"\"\"An async context manager double that behaves like a real one.\"\"\"\n",[90,133,135],{"class":92,"line":134},8,[90,136,137],{},"    cm = MagicMock()\n",[90,139,141],{"class":92,"line":140},9,[90,142,143],{},"    cm.__aenter__ = AsyncMock(return_value=value)    # what `as` binds to\n",[90,145,147],{"class":92,"line":146},10,[90,148,149],{},"    cm.__aexit__ = AsyncMock(return_value=False)     # False: exceptions propagate\n",[90,151,153],{"class":92,"line":152},11,[90,154,155],{},"    return cm\n",[90,157,159],{"class":92,"line":158},12,[90,160,103],{"emptyLinePlaceholder":102},[90,162,164],{"class":92,"line":163},13,[90,165,103],{"emptyLinePlaceholder":102},[90,167,169],{"class":92,"line":168},14,[90,170,171],{},"async def test_failed_query_still_releases_the_connection(repo):\n",[90,173,175],{"class":92,"line":174},15,[90,176,177],{},"    connection = AsyncMock()\n",[90,179,181],{"class":92,"line":180},16,[90,182,183],{},"    connection.fetch.side_effect = RuntimeError(\"query failed\")\n",[90,185,187],{"class":92,"line":186},17,[90,188,189],{},"    repo.pool.acquire = MagicMock(return_value=async_cm(connection))\n",[90,191,193],{"class":92,"line":192},18,[90,194,103],{"emptyLinePlaceholder":102},[90,196,198],{"class":92,"line":197},19,[90,199,200],{},"    with pytest.raises(RuntimeError, match=\"query failed\"):\n",[90,202,204],{"class":92,"line":203},20,[90,205,206],{},"        await repo.load_orders(\"cus_1\")\n",[90,208,210],{"class":92,"line":209},21,[90,211,103],{"emptyLinePlaceholder":102},[90,213,215],{"class":92,"line":214},22,[90,216,217],{},"    manager = repo.pool.acquire.return_value\n",[90,219,221],{"class":92,"line":220},23,[90,222,223],{},"    manager.__aexit__.assert_awaited_once()            # cleanup ran on the failure path\n",[90,225,227],{"class":92,"line":226},24,[90,228,229],{},"    exc_type, exc, _tb = manager.__aexit__.await_args.args\n",[90,231,233],{"class":92,"line":232},25,[90,234,235],{},"    assert exc_type is RuntimeError                    # and saw the real exception\n",[237,238,241,379],"figure",{"className":239},[240],"diagram",[242,243,250,251,250,255,250,259,250,277,250,285,250,294,250,303,250,309,250,313,250,319,250,326,250,330,250,334,250,338,250,344,250,348,250,352,250,359,250,364,250,369,250,372,250,376],"svg",{"viewBox":244,"role":245,"ariaLabelledBy":246,"xmlns":249},"0 0 820 268","img",[247,248],"acm2-t","acm2-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[252,253,254],"title",{"id":247},"The async with protocol, step by step",[256,257,258],"desc",{"id":248},"Entering the block awaits __aenter__, whose return value is bound to the name after as. The body runs. On exit, __aexit__ is awaited with the exception type, value and traceback, or three Nones on success. If __aexit__ returns a truthy value the exception is suppressed; if it returns False the exception propagates to the caller.",[260,261,262,263,250],"defs",{},"\n    ",[264,265,272],"marker",{"id":266,"viewBox":267,"refX":268,"refY":269,"markerWidth":270,"markerHeight":270,"orient":271},"acm2-a","0 0 10 10","9","5","7","auto-start-reverse",[273,274],"path",{"d":275,"fill":276},"M0 0 L10 5 L0 10 z","#3d405b",[278,279],"rect",{"x":280,"y":280,"width":281,"height":282,"rx":283,"fill":284},"0","820","268","14","#fffdf8",[286,287,293],"text",{"x":288,"y":289,"textAnchor":290,"fontSize":291,"fontWeight":292,"fill":276},"410","28","middle","16","700","Two awaits and one decision",[278,295],{"x":296,"y":297,"width":298,"height":299,"rx":300,"fill":301,"stroke":276,"strokeWidth":302},"26","56","200","70","11","#f4f1de","1.6",[286,304,308],{"x":305,"y":306,"textAnchor":290,"fontSize":307,"fontWeight":292,"fill":276},"126","84","12","await __aenter__()",[286,310,312],{"x":305,"y":311,"textAnchor":290,"fontSize":300,"fill":276},"106","result bound by \"as\"",[92,314],{"x1":315,"y1":316,"x2":317,"y2":316,"stroke":276,"strokeWidth":302,"markerEnd":318},"230","91","266","url(#acm2-a)",[278,320],{"x":321,"y":297,"width":322,"height":299,"rx":300,"fill":323,"stroke":324,"strokeWidth":325},"272","180","#e6f0ea","#81b29a","2",[286,327,329],{"x":328,"y":306,"textAnchor":290,"fontSize":307,"fontWeight":292,"fill":276},"362","body runs",[286,331,333],{"x":328,"y":311,"textAnchor":290,"fontSize":300,"fill":332},"#2a5f49","may raise",[92,335],{"x1":336,"y1":316,"x2":337,"y2":316,"stroke":276,"strokeWidth":302,"markerEnd":318},"456","492",[278,339],{"x":340,"y":297,"width":341,"height":299,"rx":300,"fill":342,"stroke":343,"strokeWidth":325},"498","296","#f7f0da","#f2cc8f",[286,345,347],{"x":346,"y":306,"textAnchor":290,"fontSize":307,"fontWeight":292,"fill":276},"646","await __aexit__(type, exc, tb)",[286,349,351],{"x":346,"y":311,"textAnchor":290,"fontSize":300,"fill":350},"#8a5a00","return value decides the exception's fate",[278,353],{"x":296,"y":354,"width":355,"height":356,"rx":300,"fill":357,"stroke":358,"strokeWidth":325},"156","368","82","#fbe9e3","#e07a5f",[286,360,363],{"x":361,"y":362,"textAnchor":290,"fontSize":307,"fontWeight":292,"fill":276},"210","182","returns a Mock (truthy)",[286,365,368],{"x":361,"y":366,"textAnchor":290,"fontSize":300,"fill":367},"204","#8f3d22","exception suppressed — test passes wrongly",[278,370],{"x":371,"y":354,"width":355,"height":356,"rx":300,"fill":323,"stroke":324,"strokeWidth":325},"426",[286,373,375],{"x":374,"y":362,"textAnchor":290,"fontSize":307,"fontWeight":292,"fill":276},"610","returns False",[286,377,378],{"x":374,"y":366,"textAnchor":290,"fontSize":300,"fill":332},"exception propagates — as in production",[380,381,382,383,385],"figcaption",{},"The left-hand outcome is the default for an unconfigured mock, which is why every async context manager double needs ",[13,384,27],{}," set explicitly.",[36,387,389],{"id":388},"why-this-works","Why this works",[10,391,392,395,396,398,399,401,402,405,406,408,409,412,413,415],{},[13,393,394],{},"async with"," expands to an await on ",[13,397,23],{},", the body, and an await on ",[13,400,27],{}," with the exception triple — three ",[13,403,404],{},"None"," values on success. Python then checks the truthiness of what ",[13,407,27],{}," returned: truthy means \"I handled it, suppress the exception\", falsy means \"let it propagate\". A ",[13,410,411],{},"MagicMock","'s auto-created ",[13,414,27],{}," returns another mock object, which is truthy, so every exception raised in the body disappears.",[10,417,418,419,421,422,425],{},"Setting ",[13,420,27],{}," to an ",[13,423,424],{},"AsyncMock(return_value=False)"," restores the real manager's behaviour for any manager that does not deliberately suppress exceptions — which is nearly all of them. Recording the call also gives the test something to assert on: that cleanup was awaited, and with which exception, which is the evidence that the resource would have been released in production.",[10,427,428,429,431,432,434,435,437,438,442,443,446,447,450,451,453,454,456,457,459],{},"The binding of ",[13,430,19],{}," deserves the same care. Python evaluates the expression after ",[13,433,394],{},", awaits its ",[13,436,23],{},", and binds the ",[439,440,441],"em",{},"result"," — so ",[13,444,445],{},"async with pool.acquire() as conn"," gives ",[13,448,449],{},"conn"," whatever ",[13,452,23],{}," returned, not the manager. A double that returns itself from ",[13,455,23],{}," works for managers like locks, where the manager and the resource are the same object, and silently misleads for pools and sessions, where the body expects a different object entirely. Configuring ",[13,458,23],{},"'s return value explicitly, every time, removes the ambiguity.",[36,461,463],{"id":462},"edge-cases-and-failure-modes","Edge cases and failure modes",[41,465,466,476,490,517,541],{},[44,467,468,475],{},[469,470,471,472,474],"strong",{},"Forgetting ",[13,473,23],{},"'s return value."," The body receives a generic mock instead of the connection it expects, and assertions on it test nothing. Always set it explicitly.",[44,477,478,481,482,485,486,489],{},[469,479,480],{},"Managers that should suppress."," A few managers — ",[13,483,484],{},"contextlib.suppress",", some retry helpers — legitimately return ",[13,487,488],{},"True",". Mirror that deliberately and name it in the test.",[44,491,492,498,499,502,503,506,507,510,511,513,514,516],{},[469,493,494,497],{},[13,495,496],{},"acquire()"," being awaited itself."," Some APIs are ",[13,500,501],{},"async with await pool.acquire()"," rather than ",[13,504,505],{},"async with pool.acquire()",". The first needs ",[13,508,509],{},"acquire"," to be an ",[13,512,49],{}," returning the manager; the second needs a plain ",[13,515,411],{},". Match the real API.",[44,518,519,522,523,526,527,530,531,533,534,530,537,540],{},[469,520,521],{},"Nested managers."," A transaction inside a connection inside a pool needs each level configured. The helper composes: ",[13,524,525],{},"async_cm(async_cm(transaction))"," is not right, but returning ",[13,528,529],{},"async_cm(conn)"," from ",[13,532,509],{}," and ",[13,535,536],{},"async_cm(tx)",[13,538,539],{},"conn.transaction"," is.",[44,542,543,546,547,549,550,552],{},[469,544,545],{},"Using a real manager when available."," For your own managers, a fake implementation with real ",[13,548,23],{},"\u002F",[13,551,27],{}," methods is often clearer than a mock and cannot get the protocol wrong.",[36,554,556],{"id":555},"testing-both-exit-paths","Testing both exit paths",[10,558,559,560,562,563,565,566,568],{},"A context manager exists to guarantee cleanup, and the guarantee has two halves. The success path — body completes, ",[13,561,27],{}," receives three ",[13,564,404],{},"s, resources are released — is what every test exercises by accident. The failure path — body raises, ",[13,567,27],{}," receives the exception, resources are still released, the exception still reaches the caller — is what production exercises during an incident and tests exercise almost never.",[10,570,571,572,574],{},"Writing both as explicit tests turns the guarantee into something the suite checks rather than something the code claims. The failure-path test is the more valuable of the two, and it should assert three things: the exception propagated to the caller, ",[13,573,27],{}," was awaited exactly once, and it received the real exception type. That last check catches a subtle bug where intermediate code catches the original exception and raises a different one, leaving the manager to clean up with the wrong information — harmless for a connection, but significant for a transaction manager that decides between commit and rollback based on whether an exception occurred.",[10,576,577,578,533,581,584,585,587],{},"A third test is worth adding for any manager wrapping a transaction: that on the success path the commit happened and on the failure path the rollback did. With a hand-configured double this is a matter of giving the transaction mock ",[13,579,580],{},"commit",[13,582,583],{},"rollback"," as ",[13,586,49],{},"s and asserting which one was awaited. It is the test that proves the manager does its job rather than merely that Python called its methods, and it is the one that fails when someone refactors the manager and gets the condition backwards.",[237,589,591,659],{"className":590},[240],[242,592,250,597,250,600,250,603,250,607,250,612,250,617,250,623,250,627,250,631,250,635,250,639,250,642,250,646,250,650,250,653,250,656],{"viewBox":593,"role":245,"ariaLabelledBy":594,"xmlns":249},"0 0 800 244",[595,596],"paths-t","paths-d",[252,598,599],{"id":595},"Assertions for the success and failure exits",[256,601,602],{"id":596},"Two columns of assertions. On the success path, __aexit__ is awaited with three Nones and the transaction's commit is awaited. On the failure path, the exception reaches the caller, __aexit__ is awaited with the real exception type, and the transaction's rollback is awaited instead of commit.",[278,604],{"x":280,"y":280,"width":605,"height":606,"rx":283,"fill":284},"800","244",[286,608,611],{"x":609,"y":289,"textAnchor":290,"fontSize":610,"fontWeight":292,"fill":276},"400","15.5","Two exits, two sets of assertions",[278,613],{"x":296,"y":614,"width":615,"height":616,"rx":307,"fill":323,"stroke":324,"strokeWidth":325},"50","360","172",[286,618,622],{"x":619,"y":620,"textAnchor":290,"fontSize":621,"fontWeight":292,"fill":276},"206","76","12.5","body succeeds",[286,624,626],{"x":625,"y":311,"fontSize":300,"fill":276},"44","__aexit__ awaited with (None, None, None)",[286,628,630],{"x":625,"y":629,"fontSize":300,"fill":276},"130","transaction.commit awaited",[286,632,634],{"x":625,"y":633,"fontSize":300,"fill":276},"154","rollback not awaited",[286,636,638],{"x":625,"y":637,"fontSize":300,"fill":332},"194","the path every test hits by accident",[278,640],{"x":641,"y":614,"width":615,"height":616,"rx":307,"fill":357,"stroke":358,"strokeWidth":325},"414",[286,643,645],{"x":644,"y":620,"textAnchor":290,"fontSize":621,"fontWeight":292,"fill":276},"594","body raises",[286,647,649],{"x":648,"y":311,"fontSize":300,"fill":276},"432","exception reaches the caller",[286,651,652],{"x":648,"y":629,"fontSize":300,"fill":276},"__aexit__ saw the real exception type",[286,654,655],{"x":648,"y":633,"fontSize":300,"fill":276},"rollback awaited, commit not",[286,657,658],{"x":648,"y":637,"fontSize":300,"fill":367},"the path production takes in an incident",[380,660,661],{},"The right-hand column is the one to write first. It covers the behaviour that matters most and is exercised least.",[36,663,665],{"id":664},"a-fake-manager-instead-of-a-mock","A fake manager instead of a mock",[10,667,668],{},"For managers you own, a small fake class is often clearer than any mock configuration, because it implements the protocol in ordinary Python and cannot get the suppression rule wrong by accident.",[81,670,672],{"className":83,"code":671,"language":85,"meta":86,"style":86},"class FakeConnection:\n    def __init__(self):\n        self.executed: list[str] = []\n        self.released = False\n        self.exit_exception: type | None = None\n\n    async def execute(self, sql: str) -> None:\n        self.executed.append(sql)\n\n\nclass FakePool:\n    def __init__(self):\n        self.connection = FakeConnection()\n\n    def acquire(self):\n        return self                              # the pool is its own manager here\n\n    async def __aenter__(self):\n        return self.connection\n\n    async def __aexit__(self, exc_type, exc, tb):\n        self.connection.released = True\n        self.connection.exit_exception = exc_type\n        return False                             # never suppress\n",[13,673,674,679,684,689,694,699,703,708,713,717,721,726,730,735,739,744,749,753,758,763,767,772,777,782],{"__ignoreMap":86},[90,675,676],{"class":92,"line":93},[90,677,678],{},"class FakeConnection:\n",[90,680,681],{"class":92,"line":99},[90,682,683],{},"    def __init__(self):\n",[90,685,686],{"class":92,"line":106},[90,687,688],{},"        self.executed: list[str] = []\n",[90,690,691],{"class":92,"line":112},[90,692,693],{},"        self.released = False\n",[90,695,696],{"class":92,"line":117},[90,697,698],{},"        self.exit_exception: type | None = None\n",[90,700,701],{"class":92,"line":122},[90,702,103],{"emptyLinePlaceholder":102},[90,704,705],{"class":92,"line":128},[90,706,707],{},"    async def execute(self, sql: str) -> None:\n",[90,709,710],{"class":92,"line":134},[90,711,712],{},"        self.executed.append(sql)\n",[90,714,715],{"class":92,"line":140},[90,716,103],{"emptyLinePlaceholder":102},[90,718,719],{"class":92,"line":146},[90,720,103],{"emptyLinePlaceholder":102},[90,722,723],{"class":92,"line":152},[90,724,725],{},"class FakePool:\n",[90,727,728],{"class":92,"line":158},[90,729,683],{},[90,731,732],{"class":92,"line":163},[90,733,734],{},"        self.connection = FakeConnection()\n",[90,736,737],{"class":92,"line":168},[90,738,103],{"emptyLinePlaceholder":102},[90,740,741],{"class":92,"line":174},[90,742,743],{},"    def acquire(self):\n",[90,745,746],{"class":92,"line":180},[90,747,748],{},"        return self                              # the pool is its own manager here\n",[90,750,751],{"class":92,"line":186},[90,752,103],{"emptyLinePlaceholder":102},[90,754,755],{"class":92,"line":192},[90,756,757],{},"    async def __aenter__(self):\n",[90,759,760],{"class":92,"line":197},[90,761,762],{},"        return self.connection\n",[90,764,765],{"class":92,"line":203},[90,766,103],{"emptyLinePlaceholder":102},[90,768,769],{"class":92,"line":209},[90,770,771],{},"    async def __aexit__(self, exc_type, exc, tb):\n",[90,773,774],{"class":92,"line":214},[90,775,776],{},"        self.connection.released = True\n",[90,778,779],{"class":92,"line":220},[90,780,781],{},"        self.connection.exit_exception = exc_type\n",[90,783,784],{"class":92,"line":226},[90,785,786],{},"        return False                             # never suppress\n",[10,788,789,790,793,794,797],{},"Tests then read naturally — ",[13,791,792],{},"assert pool.connection.released",", ",[13,795,796],{},"assert pool.connection.exit_exception is RuntimeError"," — with no knowledge of how mocks record awaits. The fake is also reusable across every test that needs a pool, which is where it pays back over repeated mock configuration, and it can be checked against the real pool with a small contract suite in the same way as any other fake.",[10,799,800,801,54],{},"The trade-off is a few dozen lines of test support code. For a manager used in one or two tests, the helper function above is enough. For the connection pool, the database session or the HTTP client that half the suite depends on, the fake is the better long-term choice — it is written once, reviewed once, and every test that uses it inherits a correct protocol implementation without having to know what one looks like, for the same reasons argued in ",[62,802,804],{"href":803},"\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",[237,806,808,856],{"className":807},[240],[242,809,250,814,250,817,250,820,250,823,250,826,250,828,250,831,250,835,250,838,250,842,250,844,250,847,250,850,250,853],{"viewBox":810,"role":245,"ariaLabelledBy":811,"xmlns":249},"0 0 800 226",[812,813],"fk2-t","fk2-d",[252,815,816],{"id":812},"Mock helper versus fake manager",[256,818,819],{"id":813},"Two approaches. The mock helper configures __aenter__ and __aexit__ on a MagicMock and suits managers used in a few tests. The fake manager implements the protocol in a small class that records release and the exit exception as plain attributes, suiting managers used widely across the suite.",[278,821],{"x":280,"y":280,"width":605,"height":822,"rx":283,"fill":284},"226",[286,824,825],{"x":609,"y":289,"textAnchor":290,"fontSize":610,"fontWeight":292,"fill":276},"Choose by how many tests need the manager",[278,827],{"x":296,"y":614,"width":615,"height":354,"rx":307,"fill":342,"stroke":343,"strokeWidth":325},[286,829,830],{"x":619,"y":620,"textAnchor":290,"fontSize":621,"fontWeight":292,"fill":276},"async_cm(value) helper",[286,832,834],{"x":625,"y":833,"fontSize":300,"fill":276},"104","a few lines, no new class",[286,836,837],{"x":625,"y":305,"fontSize":300,"fill":276},"asserts via await_args",[286,839,841],{"x":625,"y":840,"fontSize":300,"fontWeight":292,"fill":350},"170","a manager used in a few tests",[278,843],{"x":641,"y":614,"width":615,"height":354,"rx":307,"fill":323,"stroke":324,"strokeWidth":325},[286,845,846],{"x":644,"y":620,"textAnchor":290,"fontSize":621,"fontWeight":292,"fill":276},"FakePool class",[286,848,849],{"x":648,"y":833,"fontSize":300,"fill":276},"plain attributes: released,",[286,851,852],{"x":648,"y":305,"fontSize":300,"fill":276},"exit_exception, executed",[286,854,855],{"x":648,"y":840,"fontSize":300,"fontWeight":292,"fill":332},"a manager half the suite uses",[380,857,858],{},"Both get the protocol right. The fake additionally makes the assertions read as statements about the resource rather than about mock bookkeeping.",[36,860,862],{"id":861},"frequently-asked-questions","Frequently Asked Questions",[10,864,865,868,869,871,872,874,875,877,878,881,882,884,885,421,887,889,890,893],{},[469,866,867],{},"Why does my test pass even though the body raised inside async with?","\nBecause the mocked ",[13,870,27],{}," returned a truthy value. An unconfigured ",[13,873,411],{}," or ",[13,876,49],{}," returns a ",[13,879,880],{},"Mock"," object, which is truthy, and a truthy return from ",[13,883,27],{}," tells Python to suppress the exception. Set ",[13,886,27],{},[13,888,49],{}," with ",[13,891,892],{},"return_value=False"," so exceptions propagate as they would with the real manager.",[10,895,896,899,900,902,903,905,906,908],{},[469,897,898],{},"What does async with bind the target variable to?","\nThe value returned by awaiting ",[13,901,23],{},", not the manager object itself. If a test configures the manager but forgets ",[13,904,23],{},"'s return value, the body receives a generic ",[13,907,880],{}," rather than the connection or session it expects, and assertions on that object test nothing meaningful.",[10,910,911,914,915,918,919,533,921,923,924,926,927,929,930,932],{},[469,912,913],{},"Can autospec handle async context managers?","\nYes. ",[13,916,917],{},"create_autospec"," on a class that defines ",[13,920,23],{},[13,922,27],{}," produces a mock with ",[13,925,49],{}," versions of both, with the real signatures. You still need to set ",[13,928,23],{},"'s return value, but ",[13,931,27],{},"'s signature is enforced and the protocol is correctly async.",[36,934,936],{"id":935},"related","Related",[41,938,939,945,952,959],{},[44,940,941,944],{},[62,942,943],{"href":71},"Patching Async Code & Coroutines"," — the wider rules for async doubles.",[44,946,947,951],{},[62,948,950],{"href":949},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-async-code-and-coroutines\u002Fasserting-await-order-with-asyncmock\u002F","Asserting Await Order with AsyncMock"," — checking the sequence of enter, body and exit calls.",[44,953,954,958],{},[62,955,957],{"href":956},"\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Ftesting-async-generators-and-context-managers\u002F","Testing Async Generators and Context Managers"," — testing a real manager rather than a double.",[44,960,961,965],{},[62,962,964],{"href":963},"\u002Fintegration-database-and-service-testing\u002Fdatabase-fixtures-and-transactional-tests\u002Frolling-back-every-test-with-nested-transactions\u002F","Rolling Back Every Test with Nested Transactions"," — when the real transaction manager is the better choice.",[10,967,968,969],{},"← Back to ",[62,970,943],{"href":71},[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":86,"searchDepth":99,"depth":99,"links":976},[977,978,979,980,981,982,983,984],{"id":38,"depth":99,"text":39},{"id":75,"depth":99,"text":76},{"id":388,"depth":99,"text":389},{"id":462,"depth":99,"text":463},{"id":555,"depth":99,"text":556},{"id":664,"depth":99,"text":665},{"id":861,"depth":99,"text":862},{"id":935,"depth":99,"text":936},"Replace an async context manager in a test without swallowing exceptions: configuring __aenter__ and __aexit__, autospec for async managers, and asserting cleanup ran.","md",{"slug":988,"type":989,"breadcrumb":990,"datePublished":991,"dateModified":991,"faq":992,"howto":999},"patching-an-async-context-manager","article","Async Context Managers","2026-09-18",[993,995,997],{"q":867,"a":994},"Because the mocked __aexit__ returned a truthy value. An unconfigured MagicMock or AsyncMock returns a Mock object, which is truthy, and a truthy return from __aexit__ tells Python to suppress the exception. Set __aexit__ to an AsyncMock with return_value=False so exceptions propagate as they would with the real manager.",{"q":898,"a":996},"The value returned by awaiting __aenter__, not the manager object itself. If a test configures the manager but forgets __aenter__'s return value, the body receives a generic Mock rather than the connection or session it expects, and assertions on that object test nothing meaningful.",{"q":913,"a":998},"Yes. create_autospec on a class that defines __aenter__ and __aexit__ produces a mock with AsyncMock versions of both, with the real signatures. You still need to set __aenter__'s return value, but __aexit__'s signature is enforced and the protocol is correctly async.",{"name":1000,"description":1001,"steps":1002},"How to patch an async context manager safely","Configure __aenter__ to return the object the body needs, make __aexit__ return False, and assert that cleanup ran on both the success and failure paths.",[1003,1006,1009,1012,1015],{"name":1004,"text":1005},"Build the manager double","Create a MagicMock or autospec of the manager class so the async protocol methods exist.",{"name":1007,"text":1008},"Configure __aenter__","Set __aenter__ to an AsyncMock returning the object the async with body should receive.",{"name":1010,"text":1011},"Configure __aexit__ to propagate","Set __aexit__ to an AsyncMock with return_value=False so exceptions in the body are not suppressed.",{"name":1013,"text":1014},"Wire it into the code under test","Make the factory or method that produces the manager return the double.",{"name":1016,"text":1017},"Assert on cleanup for both paths","Check that __aexit__ was awaited after a successful body and after a failing one.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-async-code-and-coroutines\u002Fpatching-an-async-context-manager",{"title":5,"description":985},"advanced-mocking-test-doubles-in-python\u002Fpatching-async-code-and-coroutines\u002Fpatching-an-async-context-manager\u002Findex","okvTBhdlAXsVNWY0Sc0Jd1Xt7XY7Fwc92JIQiOVPAPc",1789718768946]