[{"data":1,"prerenderedAt":1187},["ShallowReactive",2],{"page-\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Frunning-one-test-on-asyncio-and-trio\u002F":3},{"id":4,"title":5,"body":6,"description":1150,"extension":1151,"meta":1152,"navigation":99,"path":1183,"seo":1184,"stem":1185,"__hash__":1186},"content\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Frunning-one-test-on-asyncio-and-trio\u002Findex.md","Running One Test on asyncio and Trio",{"type":7,"value":8,"toc":1138},"minimark",[9,13,18,60,64,71,130,246,267,275,413,417,427,438,442,503,507,510,569,575,594,597,601,604,617,628,638,750,754,757,813,816,895,971,974,985,989,992,1002,1005,1053,1060,1064,1070,1084,1097,1101,1129,1134],[10,11,12],"p",{},"A library that claims asyncio and Trio support has to prove it, and the cheapest proof is running the same test body on both. AnyIO makes the backend a fixture, so parametrising that fixture turns every marked test into two items whose failures are attributed by runtime. The setup is four lines; the value is finding, on the first run, the two or three places where the code assumed asyncio's scheduler.",[14,15,17],"h2",{"id":16},"prerequisites","Prerequisites",[19,20,21,33,49],"ul",{},[22,23,24,28,29,32],"li",{},[25,26,27],"code",{},"anyio >= 4.0"," installed as ",[25,30,31],{},"anyio[trio]",", so the Trio backend actually imports.",[22,34,35,38,39,42,43,48],{},[25,36,37],{},"pytest >= 8.0",", with ",[25,40,41],{},"pytest-asyncio"," either absent or kept away from these tests — see ",[44,45,47],"a",{"href":46},"\u002Ftesting-async-and-concurrent-python\u002Fpytest-asyncio-in-depth\u002Fconfiguring-asyncio-mode-auto-versus-strict\u002F","configuring asyncio_mode",".",[22,50,51,52,55,56,48],{},"Code under test that uses AnyIO primitives rather than ",[25,53,54],{},"asyncio"," ones; the survey is in ",[44,57,59],{"href":58},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002F","testing with AnyIO and Trio",[14,61,63],{"id":62},"solution","Solution",[10,65,66,67,70],{},"Override the plugin's ",[25,68,69],{},"anyio_backend"," fixture with a parametrised one, and mark the module.",[72,73,78],"pre",{"className":74,"code":75,"language":76,"meta":77,"style":77},"language-python shiki shiki-themes github-light github-dark","# tests\u002Fanyio\u002Fconftest.py\nimport pytest\n\n\n@pytest.fixture(params=[\"asyncio\", \"trio\"])\ndef anyio_backend(request):\n    # One fixture, two parameters: every marked test becomes two items.\n    return request.param\n","python","",[25,79,80,88,94,101,106,112,118,124],{"__ignoreMap":77},[81,82,85],"span",{"class":83,"line":84},"line",1,[81,86,87],{},"# tests\u002Fanyio\u002Fconftest.py\n",[81,89,91],{"class":83,"line":90},2,[81,92,93],{},"import pytest\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],{},"@pytest.fixture(params=[\"asyncio\", \"trio\"])\n",[81,113,115],{"class":83,"line":114},6,[81,116,117],{},"def anyio_backend(request):\n",[81,119,121],{"class":83,"line":120},7,[81,122,123],{},"    # One fixture, two parameters: every marked test becomes two items.\n",[81,125,127],{"class":83,"line":126},8,[81,128,129],{},"    return request.param\n",[72,131,133],{"className":74,"code":132,"language":76,"meta":77,"style":77},"# tests\u002Fanyio\u002Ftest_pipeline.py\nimport anyio\nimport pytest\n\npytestmark = pytest.mark.anyio          # AnyIO collects every test in this file\n\n\nasync def test_worker_signals_completion():\n    done = anyio.Event()                # portable: no asyncio.Event here\n    seen: list[int] = []\n\n    async def worker():\n        seen.append(1)\n        done.set()\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(worker)\n        with anyio.fail_after(1.0):     # portable deadline, both runtimes\n            await done.wait()\n\n    assert seen == [1]\n",[25,134,135,140,145,149,153,158,162,166,171,177,183,188,194,200,206,211,217,223,229,235,240],{"__ignoreMap":77},[81,136,137],{"class":83,"line":84},[81,138,139],{},"# tests\u002Fanyio\u002Ftest_pipeline.py\n",[81,141,142],{"class":83,"line":90},[81,143,144],{},"import anyio\n",[81,146,147],{"class":83,"line":96},[81,148,93],{},[81,150,151],{"class":83,"line":103},[81,152,100],{"emptyLinePlaceholder":99},[81,154,155],{"class":83,"line":108},[81,156,157],{},"pytestmark = pytest.mark.anyio          # AnyIO collects every test in this file\n",[81,159,160],{"class":83,"line":114},[81,161,100],{"emptyLinePlaceholder":99},[81,163,164],{"class":83,"line":120},[81,165,100],{"emptyLinePlaceholder":99},[81,167,168],{"class":83,"line":126},[81,169,170],{},"async def test_worker_signals_completion():\n",[81,172,174],{"class":83,"line":173},9,[81,175,176],{},"    done = anyio.Event()                # portable: no asyncio.Event here\n",[81,178,180],{"class":83,"line":179},10,[81,181,182],{},"    seen: list[int] = []\n",[81,184,186],{"class":83,"line":185},11,[81,187,100],{"emptyLinePlaceholder":99},[81,189,191],{"class":83,"line":190},12,[81,192,193],{},"    async def worker():\n",[81,195,197],{"class":83,"line":196},13,[81,198,199],{},"        seen.append(1)\n",[81,201,203],{"class":83,"line":202},14,[81,204,205],{},"        done.set()\n",[81,207,209],{"class":83,"line":208},15,[81,210,100],{"emptyLinePlaceholder":99},[81,212,214],{"class":83,"line":213},16,[81,215,216],{},"    async with anyio.create_task_group() as tg:\n",[81,218,220],{"class":83,"line":219},17,[81,221,222],{},"        tg.start_soon(worker)\n",[81,224,226],{"class":83,"line":225},18,[81,227,228],{},"        with anyio.fail_after(1.0):     # portable deadline, both runtimes\n",[81,230,232],{"class":83,"line":231},19,[81,233,234],{},"            await done.wait()\n",[81,236,238],{"class":83,"line":237},20,[81,239,100],{"emptyLinePlaceholder":99},[81,241,243],{"class":83,"line":242},21,[81,244,245],{},"    assert seen == [1]\n",[72,247,251],{"className":248,"code":249,"language":250,"meta":77,"style":77},"language-bash shiki shiki-themes github-light github-dark","pytest tests\u002Fanyio -q\n","bash",[25,252,253],{"__ignoreMap":77},[81,254,255,259,263],{"class":83,"line":84},[81,256,258],{"class":257},"sScJk","pytest",[81,260,262],{"class":261},"sZZnC"," tests\u002Fanyio",[81,264,266],{"class":265},"sj4cs"," -q\n",[72,268,273],{"className":269,"code":271,"language":272,"meta":77},[270],"language-text","tests\u002Fanyio\u002Ftest_pipeline.py::test_worker_signals_completion[asyncio] PASSED\ntests\u002Fanyio\u002Ftest_pipeline.py::test_worker_signals_completion[trio] PASSED\n2 passed in 0.08s\n","text",[25,274,271],{"__ignoreMap":77},[276,277,280,409],"figure",{"className":278},[279],"diagram",[281,282,289,290,289,294,289,298,289,316,289,324,289,332,289,341,289,347,289,351,289,357,289,364,289,367,289,371,289,377,289,381,289,389,289,394,289,398,289,401,289,405],"svg",{"viewBox":283,"role":284,"ariaLabelledBy":285,"xmlns":288},"0 0 820 256","img",[286,287],"two-t","two-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[291,292,293],"title",{"id":286},"One test body expanded across two runtimes",[295,296,297],"desc",{"id":287},"A single marked test resolves the anyio_backend fixture, which is parametrised with asyncio and trio. Two items are produced. The asyncio item runs on an event loop with a deterministic FIFO ready queue; the trio item runs under a nursery with deliberately randomised scheduling, so ordering assumptions fail there.",[299,300,301,302,289],"defs",{},"\n    ",[303,304,311],"marker",{"id":305,"viewBox":306,"refX":307,"refY":308,"markerWidth":309,"markerHeight":309,"orient":310},"two-a","0 0 10 10","9","5","7","auto-start-reverse",[312,313],"path",{"d":314,"fill":315},"M0 0 L10 5 L0 10 z","#3d405b",[317,318],"rect",{"x":319,"y":319,"width":320,"height":321,"rx":322,"fill":323},"0","820","256","14","#fffdf8",[272,325,331],{"x":326,"y":327,"textAnchor":328,"fontSize":329,"fontWeight":330,"fill":315},"410","28","middle","16","700","Same body, two schedulers",[317,333],{"x":334,"y":335,"width":336,"height":337,"rx":338,"fill":339,"stroke":315,"strokeWidth":340},"26","98","212","62","11","#f4f1de","1.6",[272,342,346],{"x":343,"y":344,"textAnchor":328,"fontSize":345,"fontWeight":330,"fill":315},"132","124","12","test_worker_signals",[272,348,350],{"x":343,"y":349,"textAnchor":328,"fontSize":338,"fill":315},"144","@pytest.mark.anyio",[83,352],{"x1":353,"y1":354,"x2":355,"y2":354,"stroke":315,"strokeWidth":340,"markerEnd":356},"242","129","272","url(#two-a)",[317,358],{"x":359,"y":335,"width":360,"height":337,"rx":338,"fill":361,"stroke":362,"strokeWidth":363},"278","168","#f7f0da","#f2cc8f","2",[272,365,69],{"x":366,"y":344,"textAnchor":328,"fontSize":345,"fontWeight":330,"fill":315},"362",[272,368,370],{"x":366,"y":349,"textAnchor":328,"fontSize":338,"fill":369},"#8a5a00","params: 2",[83,372],{"x1":373,"y1":374,"x2":375,"y2":376,"stroke":315,"strokeWidth":340,"markerEnd":356},"450","116","498","80",[83,378],{"x1":373,"y1":379,"x2":375,"y2":380,"stroke":315,"strokeWidth":340,"markerEnd":356},"142","186",[317,382],{"x":383,"y":384,"width":385,"height":386,"rx":338,"fill":387,"stroke":388,"strokeWidth":363},"504","46","290","70","#e6f0ea","#81b29a",[272,390,393],{"x":391,"y":392,"textAnchor":328,"fontSize":345,"fontWeight":330,"fill":315},"649","72","[asyncio]",[272,395,397],{"x":391,"y":396,"textAnchor":328,"fontSize":338,"fill":315},"94","FIFO ready queue · deterministic order",[317,399],{"x":383,"y":400,"width":385,"height":386,"rx":338,"fill":387,"stroke":388,"strokeWidth":363},"152",[272,402,404],{"x":391,"y":403,"textAnchor":328,"fontSize":345,"fontWeight":330,"fill":315},"178","[trio]",[272,406,408],{"x":391,"y":407,"textAnchor":328,"fontSize":338,"fill":315},"200","randomised order · finds ordering assumptions",[410,411,412],"figcaption",{},"Trio's randomised scheduling is the feature that makes the second item worth running: it turns a latent ordering assumption into a reproducible failure.",[14,414,416],{"id":415},"why-this-works","Why this works",[10,418,419,420,423,424,426],{},"AnyIO's pytest plugin runs a marked coroutine test by calling ",[25,421,422],{},"anyio.run"," with the backend named by the ",[25,425,69],{}," fixture. Because that fixture is an ordinary pytest fixture, parametrising it multiplies the test items exactly as parametrising any other fixture would, and the backend name appears in the item id.",[10,428,429,430,433,434,437],{},"Everything pytest can do with parametrisation therefore applies: ",[25,431,432],{},"-k trio"," selects one runtime, ",[25,435,436],{},"pytest.param(\"trio\", marks=pytest.mark.xfail)"," records a known gap, and a failure reports which runtime it occurred on without any extra instrumentation. The plugin does not special-case any of this; it simply reads the fixture.",[14,439,441],{"id":440},"edge-cases-and-failure-modes","Edge cases and failure modes",[19,443,444,463,474,487,493],{},[22,445,446,453,454,456,457,460,461,48],{},[447,448,449,452],"strong",{},[25,450,451],{},"trio"," not installed."," The ",[25,455,451],{}," parameter raises ",[25,458,459],{},"ImportError"," at test time rather than skipping, which is correct — a silently missing matrix entry is worse than a loud one. Depend on ",[25,462,31],{},[22,464,465,470,471,473],{},[447,466,467,469],{},[25,468,41],{}," in auto mode nearby."," It will claim these tests before AnyIO sees them, and the ",[25,472,69],{}," fixture is then unused. Keep them in a directory with its own invocation.",[22,475,476,482,483,486],{},[447,477,478,479,481],{},"An ",[25,480,54],{}," import inside the test body."," ",[25,484,485],{},"asyncio.get_running_loop()"," raises on the Trio item. Move backend-specific assertions into a pinned module.",[22,488,489,492],{},[447,490,491],{},"Fixtures that are async."," They run on the same backend as the requesting test, which is correct, but it means an async fixture cannot be session-scoped across differing backends. Keep expensive setup synchronous.",[22,494,495,498,499,502],{},[447,496,497],{},"Time-based assertions."," Trio and asyncio schedule timers differently enough that a tight ",[25,500,501],{},"assert elapsed \u003C 0.05"," will eventually fail on one of them. Assert on ordering and events instead.",[14,504,506],{"id":505},"passing-options-to-a-backend","Passing options to a backend",[10,508,509],{},"The fixture may return a tuple of the backend name and an options dictionary, which is how uvloop and Trio's own knobs are configured.",[72,511,513],{"className":74,"code":512,"language":76,"meta":77,"style":77},"import pytest\n\nBACKENDS = [\n    pytest.param((\"asyncio\", {\"use_uvloop\": False}), id=\"asyncio\"),\n    pytest.param((\"asyncio\", {\"use_uvloop\": True}), id=\"uvloop\"),\n    pytest.param((\"trio\", {}), id=\"trio\"),\n]\n\n\n@pytest.fixture(params=BACKENDS)\ndef anyio_backend(request):\n    return request.param\n",[25,514,515,519,523,528,533,538,543,548,552,556,561,565],{"__ignoreMap":77},[81,516,517],{"class":83,"line":84},[81,518,93],{},[81,520,521],{"class":83,"line":90},[81,522,100],{"emptyLinePlaceholder":99},[81,524,525],{"class":83,"line":96},[81,526,527],{},"BACKENDS = [\n",[81,529,530],{"class":83,"line":103},[81,531,532],{},"    pytest.param((\"asyncio\", {\"use_uvloop\": False}), id=\"asyncio\"),\n",[81,534,535],{"class":83,"line":108},[81,536,537],{},"    pytest.param((\"asyncio\", {\"use_uvloop\": True}), id=\"uvloop\"),\n",[81,539,540],{"class":83,"line":114},[81,541,542],{},"    pytest.param((\"trio\", {}), id=\"trio\"),\n",[81,544,545],{"class":83,"line":120},[81,546,547],{},"]\n",[81,549,550],{"class":83,"line":126},[81,551,100],{"emptyLinePlaceholder":99},[81,553,554],{"class":83,"line":173},[81,555,100],{"emptyLinePlaceholder":99},[81,557,558],{"class":83,"line":179},[81,559,560],{},"@pytest.fixture(params=BACKENDS)\n",[81,562,563],{"class":83,"line":185},[81,564,117],{},[81,566,567],{"class":83,"line":190},[81,568,129],{},[72,570,573],{"className":571,"code":572,"language":272,"meta":77},[270],"tests\u002Fanyio\u002Ftest_pipeline.py::test_worker_signals_completion[asyncio] PASSED\ntests\u002Fanyio\u002Ftest_pipeline.py::test_worker_signals_completion[uvloop]  PASSED\ntests\u002Fanyio\u002Ftest_pipeline.py::test_worker_signals_completion[trio]    PASSED\n",[25,574,572],{"__ignoreMap":77},[10,576,577,578,581,582,585,586,589,590,48],{},"Explicit ",[25,579,580],{},"id"," values matter here. Without them the tuple's ",[25,583,584],{},"repr"," becomes the parameter id, producing identifiers such as ",[25,587,588],{},"test_worker[anyio_backend0]"," that are useless in a report and unstable across refactors — the readable-id problem covered in ",[44,591,593],{"href":592},"\u002Fadvanced-pytest-architecture-configuration\u002Fadvanced-parametrization-techniques\u002Fgenerating-readable-test-ids\u002F","generating readable test IDs",[10,595,596],{},"Three backends is usually one too many for every test. A practical arrangement runs asyncio and Trio everywhere, and adds the uvloop entry only for the handful of tests that exercise socket handling, where uvloop's different implementation genuinely could diverge.",[14,598,600],{"id":599},"reading-a-backend-specific-failure","Reading a backend-specific failure",[10,602,603],{},"When one item fails and its sibling passes, the diagnosis is nearly always one of three things, and they are distinguishable from the traceback alone.",[10,605,478,606,616],{},[447,607,608,611,612,615],{},[25,609,610],{},"AttributeError"," or ",[25,613,614],{},"RuntimeError"," naming an asyncio API"," means backend-specific code leaked into a portable test. The fix is to replace the call with its AnyIO equivalent or to pin the test.",[10,618,619,620,623,624,627],{},"A ",[447,621,622],{},"timeout or a hang on Trio only"," usually means the code depends on a task being scheduled before another. Trio randomises that order deliberately, so the failure is the bug surfacing rather than Trio misbehaving. The repair is a real synchronisation primitive — an ",[25,625,626],{},"anyio.Event",", a memory object stream — in place of the implicit ordering.",[10,629,619,630,633,634,637],{},[447,631,632],{},"cancellation-shaped failure",", where cleanup did not run or an exception changed type, points at a difference in how the two runtimes deliver cancellation. Trio's cancellation is level-triggered within a cancel scope; asyncio's is edge-triggered per task. Code that catches ",[25,635,636],{},"BaseException"," broadly behaves differently under the two, which is worth knowing about regardless of which runtime ships.",[276,639,641,747],{"className":640},[279],[281,642,289,647,289,650,289,653,289,657,289,662,289,669,289,672,289,676,289,681,289,685,289,690,289,694,289,697,289,700,289,702,289,705,289,709,289,712,289,715,289,718,289,721,289,724,289,726,289,730,289,734,289,737,289,741,289,744],{"viewBox":643,"role":284,"ariaLabelledBy":644,"xmlns":288},"0 0 800 244",[645,646],"diag-t","diag-d",[291,648,649],{"id":645},"Three shapes of backend-specific failure",[295,651,652],{"id":646},"Three cards. An attribute or runtime error naming an asyncio API means backend-specific code leaked into a portable test. A hang or timeout on Trio only means the code relied on scheduling order. A cancellation-shaped failure points at the different cancellation semantics of the two runtimes.",[317,654],{"x":319,"y":319,"width":655,"height":656,"rx":322,"fill":323},"800","244",[272,658,661],{"x":659,"y":327,"textAnchor":328,"fontSize":660,"fontWeight":330,"fill":315},"400","15.5","One item red, its sibling green",[317,663],{"x":664,"y":665,"width":666,"height":667,"rx":345,"fill":323,"stroke":668,"strokeWidth":363},"24","50","240","176","#e07a5f",[317,670],{"x":664,"y":665,"width":666,"height":671,"rx":345,"fill":315},"30",[272,673,675],{"x":349,"y":386,"textAnchor":328,"fontSize":674,"fontWeight":330,"fill":323},"11.5","asyncio API named",[272,677,680],{"x":678,"y":679,"fontSize":338,"fill":315},"40","104","get_running_loop,",[272,682,684],{"x":678,"y":683,"fontSize":338,"fill":315},"126","call_soon, all_tasks",[272,686,689],{"x":678,"y":687,"fontSize":338,"fontWeight":330,"fill":688},"154","#8f3d22","leaked into the body",[272,691,693],{"x":678,"y":692,"fontSize":338,"fill":315},"180","fix: use the anyio",[272,695,696],{"x":678,"y":407,"fontSize":338,"fill":315},"equivalent, or pin",[317,698],{"x":699,"y":665,"width":666,"height":667,"rx":345,"fill":323,"stroke":362,"strokeWidth":363},"280",[317,701],{"x":699,"y":665,"width":666,"height":671,"rx":345,"fill":315},[272,703,704],{"x":659,"y":386,"textAnchor":328,"fontSize":674,"fontWeight":330,"fill":323},"hangs on trio only",[272,706,708],{"x":707,"y":679,"fontSize":338,"fill":315},"296","task A assumed to run",[272,710,711],{"x":707,"y":683,"fontSize":338,"fill":315},"before task B",[272,713,714],{"x":707,"y":687,"fontSize":338,"fontWeight":330,"fill":369},"a real ordering bug",[272,716,717],{"x":707,"y":692,"fontSize":338,"fill":315},"fix: an explicit event",[272,719,720],{"x":707,"y":407,"fontSize":338,"fill":315},"or a stream rendezvous",[317,722],{"x":723,"y":665,"width":666,"height":667,"rx":345,"fill":323,"stroke":388,"strokeWidth":363},"536",[317,725],{"x":723,"y":665,"width":666,"height":671,"rx":345,"fill":315},[272,727,729],{"x":728,"y":386,"textAnchor":328,"fontSize":674,"fontWeight":330,"fill":323},"656","cleanup differs",[272,731,733],{"x":732,"y":679,"fontSize":338,"fill":315},"552","finally skipped, or the",[272,735,736],{"x":732,"y":683,"fontSize":338,"fill":315},"exception type changed",[272,738,740],{"x":732,"y":687,"fontSize":338,"fontWeight":330,"fill":739},"#2a5f49","cancellation semantics",[272,742,743],{"x":732,"y":692,"fontSize":338,"fill":315},"fix: stop catching",[272,745,746],{"x":732,"y":407,"fontSize":338,"fill":315},"BaseException broadly",[410,748,749],{},"The middle card is the most valuable outcome of running two backends: an ordering assumption that would have survived years of asyncio-only testing.",[14,751,753],{"id":752},"fixtures-across-two-backends","Fixtures across two backends",[10,755,756],{},"Async fixtures run on whichever backend the requesting test resolved to, which is convenient and imposes one real constraint: a fixture cannot be shared across items that resolved differently.",[72,758,760],{"className":74,"code":759,"language":76,"meta":77,"style":77},"import anyio\nimport pytest\n\n\n@pytest.fixture\nasync def broker():\n    # Runs twice per test function — once per backend item — because each\n    # item gets its own runtime and therefore its own fixture instance.\n    send, receive = anyio.create_memory_object_stream[dict](max_buffer_size=4)\n    async with send, receive:\n        yield send, receive\n",[25,761,762,766,770,774,778,783,788,793,798,803,808],{"__ignoreMap":77},[81,763,764],{"class":83,"line":84},[81,765,144],{},[81,767,768],{"class":83,"line":90},[81,769,93],{},[81,771,772],{"class":83,"line":96},[81,773,100],{"emptyLinePlaceholder":99},[81,775,776],{"class":83,"line":103},[81,777,100],{"emptyLinePlaceholder":99},[81,779,780],{"class":83,"line":108},[81,781,782],{},"@pytest.fixture\n",[81,784,785],{"class":83,"line":114},[81,786,787],{},"async def broker():\n",[81,789,790],{"class":83,"line":120},[81,791,792],{},"    # Runs twice per test function — once per backend item — because each\n",[81,794,795],{"class":83,"line":126},[81,796,797],{},"    # item gets its own runtime and therefore its own fixture instance.\n",[81,799,800],{"class":83,"line":173},[81,801,802],{},"    send, receive = anyio.create_memory_object_stream[dict](max_buffer_size=4)\n",[81,804,805],{"class":83,"line":179},[81,806,807],{},"    async with send, receive:\n",[81,809,810],{"class":83,"line":185},[81,811,812],{},"        yield send, receive\n",[10,814,815],{},"This is correct but it means any expensive async setup is paid once per backend, doubling the cost of the very fixtures that were already the slow ones. The remedy is the same split recommended throughout this section: keep the expensive part synchronous and session-scoped, and let only a thin async handle be per-test.",[72,817,819],{"className":74,"code":818,"language":76,"meta":77,"style":77},"import pytest\nfrom testcontainers.postgres import PostgresContainer\n\n\n@pytest.fixture(scope=\"session\")\ndef dsn():\n    # Synchronous and session-scoped: no loop, no backend, no duplication.\n    with PostgresContainer(\"postgres:16-alpine\") as container:\n        yield container.get_connection_url()\n\n\n@pytest.fixture\nasync def connection(dsn):\n    # Async and per-item: cheap to build twice, once per backend.\n    async with await connect(dsn) as conn:\n        yield conn\n",[25,820,821,825,830,834,838,843,848,853,858,863,867,871,875,880,885,890],{"__ignoreMap":77},[81,822,823],{"class":83,"line":84},[81,824,93],{},[81,826,827],{"class":83,"line":90},[81,828,829],{},"from testcontainers.postgres import PostgresContainer\n",[81,831,832],{"class":83,"line":96},[81,833,100],{"emptyLinePlaceholder":99},[81,835,836],{"class":83,"line":103},[81,837,100],{"emptyLinePlaceholder":99},[81,839,840],{"class":83,"line":108},[81,841,842],{},"@pytest.fixture(scope=\"session\")\n",[81,844,845],{"class":83,"line":114},[81,846,847],{},"def dsn():\n",[81,849,850],{"class":83,"line":120},[81,851,852],{},"    # Synchronous and session-scoped: no loop, no backend, no duplication.\n",[81,854,855],{"class":83,"line":126},[81,856,857],{},"    with PostgresContainer(\"postgres:16-alpine\") as container:\n",[81,859,860],{"class":83,"line":173},[81,861,862],{},"        yield container.get_connection_url()\n",[81,864,865],{"class":83,"line":179},[81,866,100],{"emptyLinePlaceholder":99},[81,868,869],{"class":83,"line":185},[81,870,100],{"emptyLinePlaceholder":99},[81,872,873],{"class":83,"line":190},[81,874,782],{},[81,876,877],{"class":83,"line":196},[81,878,879],{},"async def connection(dsn):\n",[81,881,882],{"class":83,"line":202},[81,883,884],{},"    # Async and per-item: cheap to build twice, once per backend.\n",[81,886,887],{"class":83,"line":208},[81,888,889],{},"    async with await connect(dsn) as conn:\n",[81,891,892],{"class":83,"line":213},[81,893,894],{},"        yield conn\n",[276,896,898,968],{"className":897},[279],[281,899,289,904,289,907,289,910,289,917,289,920,289,923,289,928,289,933,289,937,289,943,289,946,289,952,289,955,289,959,289,961,289,965],{"viewBox":900,"role":284,"ariaLabelledBy":901,"xmlns":288},"0 0 800 234",[902,903],"fxb-t","fxb-d",[291,905,906],{"id":902},"Splitting setup so only the cheap half runs twice",[295,908,909],{"id":903},"A session-scoped synchronous fixture starts a container once and yields a connection string. Both the asyncio item and the trio item then build their own async connection from that string, so the expensive container start is paid once while only the cheap connection is duplicated.",[299,911,301,912,289],{},[303,913,915],{"id":914,"viewBox":306,"refX":307,"refY":308,"markerWidth":309,"markerHeight":309,"orient":310},"fxb-a",[312,916],{"d":314,"fill":315},[317,918],{"x":319,"y":319,"width":655,"height":919,"rx":322,"fill":323},"234",[272,921,922],{"x":659,"y":327,"textAnchor":328,"fontSize":660,"fontWeight":330,"fill":315},"Expensive once, cheap twice",[317,924],{"x":925,"y":926,"width":927,"height":392,"rx":338,"fill":361,"stroke":362,"strokeWidth":363},"34","82","250",[272,929,932],{"x":930,"y":931,"textAnchor":328,"fontSize":345,"fontWeight":330,"fill":315},"159","108","container (session, sync)",[272,934,936],{"x":930,"y":935,"textAnchor":328,"fontSize":338,"fill":369},"130","3 s, once per run",[83,938],{"x1":939,"y1":940,"x2":941,"y2":386,"stroke":315,"strokeWidth":340,"markerEnd":942},"288","102","336","url(#fxb-a)",[83,944],{"x1":939,"y1":945,"x2":941,"y2":667,"stroke":315,"strokeWidth":340,"markerEnd":942},"134",[317,947],{"x":948,"y":949,"width":950,"height":951,"rx":338,"fill":387,"stroke":388,"strokeWidth":363},"342","38","420","64",[272,953,954],{"x":732,"y":951,"textAnchor":328,"fontSize":345,"fontWeight":330,"fill":315},"[asyncio] connection fixture",[272,956,958],{"x":732,"y":957,"textAnchor":328,"fontSize":338,"fill":739},"86","a few milliseconds",[317,960],{"x":948,"y":349,"width":950,"height":951,"rx":338,"fill":387,"stroke":388,"strokeWidth":363},[272,962,964],{"x":732,"y":963,"textAnchor":328,"fontSize":345,"fontWeight":330,"fill":315},"170","[trio] connection fixture",[272,966,958],{"x":732,"y":967,"textAnchor":328,"fontSize":338,"fill":739},"192",[410,969,970],{},"Without the split, the container fixture would have to be async and per-item, and the backend matrix would double the slowest part of the suite rather than the fastest.",[10,972,973],{},"The same reasoning applies to anything with a handshake — a broker connection, a warmed cache, a compiled schema. Discover it once synchronously, connect to it cheaply per item, and the second backend costs milliseconds rather than seconds.",[10,975,976,977,980,981,984],{},"One consequence is worth anticipating: a synchronous session fixture cannot ",[25,978,979],{},"await"," anything, so any setup that genuinely requires the network has to run through a blocking client rather than an async one. For a container start, a schema migration or a health poll that is not a hardship — the synchronous library exists and is usually simpler. Where no synchronous path exists, ",[25,982,983],{},"anyio.from_thread.start_blocking_portal()"," gives a session-scoped portal that can run coroutines from synchronous code, at the cost of one more moving part; reach for it only when the blocking alternative is genuinely missing.",[14,986,988],{"id":987},"keeping-the-matrix-affordable","Keeping the matrix affordable",[10,990,991],{},"Every parametrised test runs twice, so the matrix has a real cost on a large suite, and two adjustments keep it proportionate.",[10,993,994,995,997,998,1001],{},"Run both backends where the runtime could plausibly matter — concurrency primitives, cancellation, streams, timeouts — and pin everything else to one. A test that posts JSON and asserts on a status code learns nothing from a second runtime, and pinning it with a local ",[25,996,69],{}," fixture returning ",[25,999,1000],{},"\"asyncio\""," costs one line per module.",[10,1003,1004],{},"Then move the full matrix off the pull-request path. Deriving the parameter list from an environment variable keeps one configuration and one set of tests, with the breadth decided per job:",[72,1006,1008],{"className":74,"code":1007,"language":76,"meta":77,"style":77},"import os\n\nimport pytest\n\n_ALL = os.environ.get(\"TEST_ALL_BACKENDS\") == \"1\"\n\n\n@pytest.fixture(params=[\"asyncio\", \"trio\"] if _ALL else [\"asyncio\"])\ndef anyio_backend(request):\n    return request.param\n",[25,1009,1010,1015,1019,1023,1027,1032,1036,1040,1045,1049],{"__ignoreMap":77},[81,1011,1012],{"class":83,"line":84},[81,1013,1014],{},"import os\n",[81,1016,1017],{"class":83,"line":90},[81,1018,100],{"emptyLinePlaceholder":99},[81,1020,1021],{"class":83,"line":96},[81,1022,93],{},[81,1024,1025],{"class":83,"line":103},[81,1026,100],{"emptyLinePlaceholder":99},[81,1028,1029],{"class":83,"line":108},[81,1030,1031],{},"_ALL = os.environ.get(\"TEST_ALL_BACKENDS\") == \"1\"\n",[81,1033,1034],{"class":83,"line":114},[81,1035,100],{"emptyLinePlaceholder":99},[81,1037,1038],{"class":83,"line":120},[81,1039,100],{"emptyLinePlaceholder":99},[81,1041,1042],{"class":83,"line":126},[81,1043,1044],{},"@pytest.fixture(params=[\"asyncio\", \"trio\"] if _ALL else [\"asyncio\"])\n",[81,1046,1047],{"class":83,"line":173},[81,1048,117],{},[81,1050,1051],{"class":83,"line":179},[81,1052,129],{},[10,1054,1055,1056,48],{},"Reading the environment at import time is acceptable here specifically because parametrisation is decided during collection; there is no later point at which the choice could be made. The result is a fast default run and a nightly job that exercises both runtimes, which is the same fast-and-thorough split used for ",[44,1057,1059],{"href":1058},"\u002Fintegration-database-and-service-testing\u002F","integration tests",[14,1061,1063],{"id":1062},"frequently-asked-questions","Frequently Asked Questions",[10,1065,1066,1069],{},[447,1067,1068],{},"Why does a test pass on asyncio and fail on Trio?","\nUsually a scheduling assumption. asyncio's ready queue is FIFO and deterministic; Trio deliberately randomises the order in which equally-ready tasks are scheduled, so code that relied on task A always running before task B fails there. That is a real bug being exposed, not a Trio incompatibility.",[10,1071,1072,1075,1076,1079,1080,1083],{},[447,1073,1074],{},"How do I run only one backend while debugging?","\nSelect by parameter id: ",[25,1077,1078],{},"pytest -k trio"," runs only the Trio items, and ",[25,1081,1082],{},"-k 'not trio'"," runs the rest. The backend is an ordinary parametrisation, so every selection mechanism pytest offers works on it.",[10,1085,1086,1089,1090,1092,1093,1096],{},[447,1087,1088],{},"Can I pass options such as uvloop to a backend?","\nYes. Return a tuple of the backend name and an options dictionary from the ",[25,1091,69],{}," fixture — for example ",[25,1094,1095],{},"(\"asyncio\", {\"use_uvloop\": True})"," — and AnyIO passes the options through to the runner.",[14,1098,1100],{"id":1099},"related","Related",[19,1102,1103,1109,1116,1123],{},[22,1104,1105,1108],{},[44,1106,1107],{"href":58},"Testing with AnyIO & Trio"," — the primitives a portable test body may use.",[22,1110,1111,1115],{},[44,1112,1114],{"href":1113},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Ftesting-code-that-uses-task-groups\u002F","Testing Code That Uses Task Groups"," — the assertions structured concurrency needs.",[22,1117,1118,1122],{},[44,1119,1121],{"href":1120},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Fporting-a-pytest-asyncio-suite-to-anyio\u002F","Porting a pytest-asyncio Suite to AnyIO"," — how to get an existing suite to this point.",[22,1124,1125,1128],{},[44,1126,1127],{"href":592},"Generating Readable Test IDs"," — why the explicit ids above matter in a report.",[10,1130,1131,1132],{},"← Back to ",[44,1133,1107],{"href":58},[1135,1136,1137],"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);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":77,"searchDepth":90,"depth":90,"links":1139},[1140,1141,1142,1143,1144,1145,1146,1147,1148,1149],{"id":16,"depth":90,"text":17},{"id":62,"depth":90,"text":63},{"id":415,"depth":90,"text":416},{"id":440,"depth":90,"text":441},{"id":505,"depth":90,"text":506},{"id":599,"depth":90,"text":600},{"id":752,"depth":90,"text":753},{"id":987,"depth":90,"text":988},{"id":1062,"depth":90,"text":1063},{"id":1099,"depth":90,"text":1100},"Parametrise the AnyIO backend so a single test body runs on asyncio and Trio, with per-backend pinning, uvloop options, and the scheduling assumptions Trio exposes.","md",{"slug":1153,"type":1154,"breadcrumb":1155,"datePublished":1156,"dateModified":1156,"faq":1157,"howto":1164},"running-one-test-on-asyncio-and-trio","article","Two Backends","2026-09-18",[1158,1160,1162],{"q":1068,"a":1159},"Usually a scheduling assumption. asyncio's ready queue is FIFO and deterministic; Trio deliberately randomises the order in which equally-ready tasks are scheduled, so code that relied on task A always running before task B fails there. That is a real bug being exposed, not a Trio incompatibility.",{"q":1074,"a":1161},"Select by parameter id: pytest -k trio runs only the Trio items, and -k 'not trio' runs the rest. The backend is an ordinary parametrisation, so every selection mechanism pytest offers works on it.",{"q":1088,"a":1163},"Yes. Return a tuple of the backend name and an options dictionary from the anyio_backend fixture — for example ('asyncio', {'use_uvloop': True}) — and AnyIO passes the options through to the runner.",{"name":1165,"description":1166,"steps":1167},"How to run one async test on both backends","Mark the module for AnyIO, parametrise the backend fixture, and keep backend-specific code out of the shared body.",[1168,1171,1174,1177,1180],{"name":1169,"text":1170},"Install both runtimes","Depend on anyio[trio] so the trio parameter resolves rather than raising ImportError at test time.",{"name":1172,"text":1173},"Mark the module","Set pytestmark to pytest.mark.anyio so AnyIO rather than pytest-asyncio collects the tests.",{"name":1175,"text":1176},"Parametrise the backend fixture","Override anyio_backend with params for asyncio and trio so each test expands into two items.",{"name":1178,"text":1179},"Replace backend-specific primitives","Use anyio.Event, memory object streams and cancel scopes in place of asyncio equivalents.",{"name":1181,"text":1182},"Pin the tests that cannot be portable","Give backend-specific modules a local anyio_backend fixture returning a single backend.","\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Frunning-one-test-on-asyncio-and-trio",{"title":5,"description":1150},"testing-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Frunning-one-test-on-asyncio-and-trio\u002Findex","Zo2OfMPYqHILkxL4yh-V-nvO5OsL3S-UbxPCavMpf5g",1789718769536]