[{"data":1,"prerenderedAt":1190},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-httpx-clients-with-respx\u002F":3},{"id":4,"title":5,"body":6,"description":1154,"extension":1155,"meta":1156,"navigation":103,"path":1186,"seo":1187,"stem":1188,"__hash__":1189},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-httpx-clients-with-respx\u002Findex.md","Mocking httpx Async Clients with respx",{"type":7,"value":8,"toc":1143},"minimark",[9,29,34,62,66,69,211,229,232,300,311,421,425,441,579,583,589,603,613,630,639,669,680,719,723,726,737,757,779,783,865,869,872,875,937,944,947,1044,1048,1061,1074,1086,1096,1100,1133,1139],[10,11,12,16,17,20,21,24,25,28],"p",{},[13,14,15],"code",{},"httpx"," is the default async HTTP client in modern Python services, and patching it the obvious way — replacing ",[13,18,19],{},"client.get"," with an ",[13,22,23],{},"AsyncMock"," — produces tests that pass while proving nothing about URL construction, headers, serialization or timeouts. ",[13,26,27],{},"respx"," intercepts one layer lower, at the transport, so the client under test builds a real request that a route then answers.",[30,31,33],"h2",{"id":32},"prerequisites","Prerequisites",[35,36,37,53],"ul",{},[38,39,40,44,45,48,49,52],"li",{},[41,42,43],"strong",{},"httpx 0.24+",", ",[41,46,47],{},"respx 0.20+",", and ",[41,50,51],{},"pytest-asyncio 0.23+"," (or anyio) for the async tests below.",[38,54,55,56,61],{},"The interception layers compared in ",[57,58,60],"a",{"href":59},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002F","mocking network and HTTP calls",".",[30,63,65],{"id":64},"solution","Solution",[10,67,68],{},"Activate the router, declare routes, and let the client run for real up to the transport boundary.",[70,71,76],"pre",{"className":72,"code":73,"language":74,"meta":75,"style":75},"language-python shiki shiki-themes github-light github-dark","import httpx\nimport pytest\nimport respx\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_fetch_orders_builds_the_right_request():\n    route = respx.get(\n        \"https:\u002F\u002Fapi.example.com\u002Forders\",\n        params={\"status\": \"open\"},                 # query is part of the match\n    ).mock(return_value=httpx.Response(200, json={\"orders\": []}))\n\n    async with httpx.AsyncClient(base_url=\"https:\u002F\u002Fapi.example.com\") as client:\n        resp = await client.get(\"\u002Forders\", params={\"status\": \"open\"},\n                                headers={\"Authorization\": \"Bearer t\"})\n\n    assert resp.status_code == 200\n    assert route.called\n    # The recorded request is a real httpx.Request: assert on what was sent.\n    sent = route.calls.last.request\n    assert sent.headers[\"Authorization\"] == \"Bearer t\"\n    assert sent.url.params[\"status\"] == \"open\"\n","python","",[13,77,78,86,92,98,105,111,117,123,129,135,141,147,152,158,164,170,175,181,187,193,199,205],{"__ignoreMap":75},[79,80,83],"span",{"class":81,"line":82},"line",1,[79,84,85],{},"import httpx\n",[79,87,89],{"class":81,"line":88},2,[79,90,91],{},"import pytest\n",[79,93,95],{"class":81,"line":94},3,[79,96,97],{},"import respx\n",[79,99,101],{"class":81,"line":100},4,[79,102,104],{"emptyLinePlaceholder":103},true,"\n",[79,106,108],{"class":81,"line":107},5,[79,109,110],{},"@respx.mock\n",[79,112,114],{"class":81,"line":113},6,[79,115,116],{},"@pytest.mark.asyncio\n",[79,118,120],{"class":81,"line":119},7,[79,121,122],{},"async def test_fetch_orders_builds_the_right_request():\n",[79,124,126],{"class":81,"line":125},8,[79,127,128],{},"    route = respx.get(\n",[79,130,132],{"class":81,"line":131},9,[79,133,134],{},"        \"https:\u002F\u002Fapi.example.com\u002Forders\",\n",[79,136,138],{"class":81,"line":137},10,[79,139,140],{},"        params={\"status\": \"open\"},                 # query is part of the match\n",[79,142,144],{"class":81,"line":143},11,[79,145,146],{},"    ).mock(return_value=httpx.Response(200, json={\"orders\": []}))\n",[79,148,150],{"class":81,"line":149},12,[79,151,104],{"emptyLinePlaceholder":103},[79,153,155],{"class":81,"line":154},13,[79,156,157],{},"    async with httpx.AsyncClient(base_url=\"https:\u002F\u002Fapi.example.com\") as client:\n",[79,159,161],{"class":81,"line":160},14,[79,162,163],{},"        resp = await client.get(\"\u002Forders\", params={\"status\": \"open\"},\n",[79,165,167],{"class":81,"line":166},15,[79,168,169],{},"                                headers={\"Authorization\": \"Bearer t\"})\n",[79,171,173],{"class":81,"line":172},16,[79,174,104],{"emptyLinePlaceholder":103},[79,176,178],{"class":81,"line":177},17,[79,179,180],{},"    assert resp.status_code == 200\n",[79,182,184],{"class":81,"line":183},18,[79,185,186],{},"    assert route.called\n",[79,188,190],{"class":81,"line":189},19,[79,191,192],{},"    # The recorded request is a real httpx.Request: assert on what was sent.\n",[79,194,196],{"class":81,"line":195},20,[79,197,198],{},"    sent = route.calls.last.request\n",[79,200,202],{"class":81,"line":201},21,[79,203,204],{},"    assert sent.headers[\"Authorization\"] == \"Bearer t\"\n",[79,206,208],{"class":81,"line":207},22,[79,209,210],{},"    assert sent.url.params[\"status\"] == \"open\"\n",[10,212,213,214,44,217,220,221,224,225,228],{},"Every route object records its own calls, which is what makes the assertions specific. ",[13,215,216],{},"route.called",[13,218,219],{},"route.call_count"," and ",[13,222,223],{},"route.calls[i].request"," cover nearly every claim a test needs to make, and because the request is a genuine ",[13,226,227],{},"httpx.Request",", base-URL joining, parameter encoding and header defaults are all exercised rather than skipped.",[10,230,231],{},"Failure paths are one argument away, which is the main reason to use a transport-level mock at all:",[70,233,235],{"className":72,"code":234,"language":74,"meta":75,"style":75},"import httpx, respx, pytest\n\n@respx.mock\n@pytest.mark.asyncio\nasync def test_retries_once_on_timeout():\n    route = respx.get(\"https:\u002F\u002Fapi.example.com\u002Forders\")\n    route.side_effect = [httpx.ConnectTimeout(\"timed out\"),\n                         httpx.Response(200, json={\"orders\": []})]\n\n    async with httpx.AsyncClient(base_url=\"https:\u002F\u002Fapi.example.com\") as client:\n        resp = await fetch_with_retry(client, \"\u002Forders\", attempts=2)\n\n    assert resp.status_code == 200\n    assert route.call_count == 2            # the retry really happened\n",[13,236,237,242,246,250,254,259,264,269,274,278,282,287,291,295],{"__ignoreMap":75},[79,238,239],{"class":81,"line":82},[79,240,241],{},"import httpx, respx, pytest\n",[79,243,244],{"class":81,"line":88},[79,245,104],{"emptyLinePlaceholder":103},[79,247,248],{"class":81,"line":94},[79,249,110],{},[79,251,252],{"class":81,"line":100},[79,253,116],{},[79,255,256],{"class":81,"line":107},[79,257,258],{},"async def test_retries_once_on_timeout():\n",[79,260,261],{"class":81,"line":113},[79,262,263],{},"    route = respx.get(\"https:\u002F\u002Fapi.example.com\u002Forders\")\n",[79,265,266],{"class":81,"line":119},[79,267,268],{},"    route.side_effect = [httpx.ConnectTimeout(\"timed out\"),\n",[79,270,271],{"class":81,"line":125},[79,272,273],{},"                         httpx.Response(200, json={\"orders\": []})]\n",[79,275,276],{"class":81,"line":131},[79,277,104],{"emptyLinePlaceholder":103},[79,279,280],{"class":81,"line":137},[79,281,157],{},[79,283,284],{"class":81,"line":143},[79,285,286],{},"        resp = await fetch_with_retry(client, \"\u002Forders\", attempts=2)\n",[79,288,289],{"class":81,"line":149},[79,290,104],{"emptyLinePlaceholder":103},[79,292,293],{"class":81,"line":154},[79,294,180],{},[79,296,297],{"class":81,"line":160},[79,298,299],{},"    assert route.call_count == 2            # the retry really happened\n",[10,301,302,303,306,307,310],{},"A list ",[13,304,305],{},"side_effect"," is consumed in order, exactly as it is on a ",[13,308,309],{},"unittest.mock"," double, so a scripted sequence of failure-then-success needs no custom callable.",[312,313,316,417],"figure",{"className":314},[315],"diagram",[317,318,325,326,325,330,325,334,325,342,325,351,325,359,325,363,325,368,325,374,325,378,325,380,325,384,325,387,325,391,325,393,325,397,325,400,325,403,325,405,325,409,325,412],"svg",{"viewBox":319,"role":320,"ariaLabelledBy":321,"xmlns":324},"0 0 760 338","img",[322,323],"respxlayers-t","respxlayers-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[327,328,329],"title",{"id":322},"What each mocking layer still exercises",[331,332,333],"desc",{"id":323},"A stack of four httpx mocking approaches ordered by depth: replacing the client method, replacing the client object, respx at the transport layer, and a real local server, showing what code each approach still runs.",[335,336],"rect",{"x":337,"y":337,"width":338,"height":339,"rx":340,"fill":341},"0","760","338","14","#fffdf8",[343,344,329],"text",{"x":345,"y":346,"textAnchor":347,"fontSize":348,"fontWeight":349,"fill":350},"380","30","middle","15","700","#3d405b",[335,352],{"x":346,"y":353,"width":349,"height":354,"rx":355,"fill":356,"stroke":357,"strokeWidth":358},"56","52","10","#f4f1de","#e07a5f","1.8",[335,360],{"x":346,"y":353,"width":361,"height":354,"rx":362,"fill":357},"8","4",[343,364,367],{"x":354,"y":365,"fontSize":366,"fontWeight":349,"fill":350},"86","12.5","AsyncMock on client.get",[343,369,373],{"x":370,"y":365,"textAnchor":371,"fontSize":372,"fill":350},"714","end","11","nothing: no URL, no headers, no serialization",[335,375],{"x":346,"y":376,"width":349,"height":354,"rx":355,"fill":341,"stroke":377,"strokeWidth":358},"120","#f2cc8f",[335,379],{"x":346,"y":376,"width":361,"height":354,"rx":362,"fill":377},[343,381,383],{"x":354,"y":382,"fontSize":366,"fontWeight":349,"fill":350},"150","a fake client object",[343,385,386],{"x":370,"y":382,"textAnchor":371,"fontSize":372,"fill":350},"your call sites only",[335,388],{"x":346,"y":389,"width":349,"height":354,"rx":355,"fill":356,"stroke":390,"strokeWidth":358},"184","#81b29a",[335,392],{"x":346,"y":389,"width":361,"height":354,"rx":362,"fill":390},[343,394,396],{"x":354,"y":395,"fontSize":366,"fontWeight":349,"fill":350},"214","respx at the transport",[343,398,399],{"x":370,"y":395,"textAnchor":371,"fontSize":372,"fill":350},"URL building, params, headers, retries",[335,401],{"x":346,"y":402,"width":349,"height":354,"rx":355,"fill":341,"stroke":350,"strokeWidth":358},"248",[335,404],{"x":346,"y":402,"width":361,"height":354,"rx":362,"fill":350},[343,406,408],{"x":354,"y":407,"fontSize":366,"fontWeight":349,"fill":350},"278","a real local server",[343,410,411],{"x":370,"y":407,"textAnchor":371,"fontSize":372,"fill":350},"everything, including the socket",[343,413,416],{"x":345,"y":414,"textAnchor":347,"fontSize":372,"fontStyle":415,"fill":350},"330","italic","The top row is the one that produces green tests against a broken URL.",[418,419,420],"figcaption",{},"Transport interception is the sweet spot: no network, but every layer of the client that a bug could hide in still runs.",[30,422,424],{"id":423},"why-this-works","Why this works",[10,426,427,429,430,433,434,437,438,440],{},[13,428,15],{}," routes every request through a transport object — ",[13,431,432],{},"HTTPTransport"," or ",[13,435,436],{},"AsyncHTTPTransport"," — that both the sync and async clients share as their final step. ",[13,439,27],{}," installs a mock transport in its place, so the client performs request construction, base-URL resolution, parameter encoding, header defaults, cookie handling and event hooks exactly as in production, and the substituted transport answers instead of opening a connection. Because the substitution happens at that single seam, the same route definitions work for both client types, and nothing in the code under test needs to know it is being tested.",[312,442,444,576],{"className":443},[315],[317,445,325,450,325,453,325,456,325,483,325,486,325,488,325,493,325,499,325,505,325,508,325,511,325,514,325,517,325,521,325,524,325,530,325,535,325,539,325,544,325,549,325,554,325,560,325,564,325,568,325,572],{"viewBox":446,"role":320,"ariaLabelledBy":447,"xmlns":324},"0 0 760 430",[448,449],"respxpath-t","respxpath-d",[327,451,452],{"id":448},"Where respx intercepts an httpx request",[331,454,455],{"id":449},"A sequence diagram with three lanes: the code under test, the httpx client, and the respx mock transport. The code calls get, the client builds a full Request object with resolved URL and headers, the mock transport matches it against declared routes and returns the declared response, and the client wraps it as a normal Response.",[457,458,459,460,459,473,459,478,325],"defs",{},"\n    ",[461,462,469],"marker",{"id":463,"viewBox":464,"refX":465,"refY":466,"markerWidth":467,"markerHeight":467,"orient":468},"respxpath-a","0 0 10 10","9","5","7","auto-start-reverse",[470,471],"path",{"d":472,"fill":350},"M0 0 L10 5 L0 10 z",[461,474,476],{"id":475,"viewBox":464,"refX":465,"refY":466,"markerWidth":467,"markerHeight":467,"orient":468},"respxpath-c",[470,477],{"d":472,"fill":357},[461,479,481],{"id":480,"viewBox":464,"refX":465,"refY":466,"markerWidth":467,"markerHeight":467,"orient":468},"respxpath-s",[470,482],{"d":472,"fill":390},[335,484],{"x":337,"y":337,"width":338,"height":485,"rx":340,"fill":341},"430",[343,487,452],{"x":345,"y":346,"textAnchor":347,"fontSize":348,"fontWeight":349,"fill":350},[335,489],{"x":490,"y":354,"width":491,"height":492,"rx":355,"fill":356,"stroke":350,"strokeWidth":358},"34","220","40",[343,494,498],{"x":495,"y":496,"textAnchor":347,"fontSize":497,"fontWeight":349,"fill":350},"144","76","12","code under test",[81,500],{"x1":495,"y1":501,"x2":495,"y2":502,"stroke":350,"strokeWidth":503,"strokeDashArray":504},"98","414","1.2",[466,466],[335,506],{"x":507,"y":354,"width":491,"height":492,"rx":355,"fill":356,"stroke":350,"strokeWidth":358},"270",[343,509,510],{"x":345,"y":496,"textAnchor":347,"fontSize":497,"fontWeight":349,"fill":350},"httpx client",[81,512],{"x1":345,"y1":501,"x2":345,"y2":502,"stroke":350,"strokeWidth":503,"strokeDashArray":513},[466,466],[335,515],{"x":516,"y":354,"width":491,"height":492,"rx":355,"fill":356,"stroke":350,"strokeWidth":358},"506",[343,518,520],{"x":519,"y":496,"textAnchor":347,"fontSize":497,"fontWeight":349,"fill":350},"616","respx transport",[81,522],{"x1":519,"y1":501,"x2":519,"y2":502,"stroke":350,"strokeWidth":503,"strokeDashArray":523},[466,466],[81,525],{"x1":526,"y1":527,"x2":528,"y2":527,"stroke":350,"strokeWidth":358,"markerEnd":529},"152","126","372","url(#respxpath-a)",[343,531,534],{"x":532,"y":533,"textAnchor":347,"fontSize":372,"fill":350},"262","117","client.get(\"\u002Forders\")",[470,536],{"d":537,"fill":538,"stroke":350,"strokeWidth":358,"markerEnd":529},"M380 180 h 46 v 22 h -40","none",[343,540,543],{"x":541,"y":389,"textAnchor":542,"fontSize":372,"fill":350},"436","start","build Request",[81,545],{"x1":546,"y1":547,"x2":548,"y2":547,"stroke":350,"strokeWidth":358,"markerEnd":529},"388","234","608",[343,550,553],{"x":551,"y":552,"textAnchor":347,"fontSize":372,"fill":350},"498","225","send(request)",[81,555],{"x1":548,"y1":556,"x2":546,"y2":556,"stroke":390,"strokeWidth":358,"strokeDashArray":557,"markerEnd":559},"288",[558,362],"6","url(#respxpath-s)",[343,561,563],{"x":551,"y":562,"textAnchor":347,"fontSize":372,"fill":350},"279","declared Response",[81,565],{"x1":528,"y1":566,"x2":526,"y2":566,"stroke":390,"strokeWidth":358,"strokeDashArray":567,"markerEnd":559},"342",[558,362],[343,569,571],{"x":532,"y":570,"textAnchor":347,"fontSize":372,"fill":350},"333","Response object",[343,573,575],{"x":345,"y":574,"textAnchor":347,"fontSize":372,"fontStyle":415,"fill":350},"418","The recorded Request is the same object the network would have received.",[418,577,578],{},"Everything left of the transport is real code — which is precisely the code a URL or header bug lives in.",[30,580,582],{"id":581},"routing-rules-that-keep-tests-honest","Routing rules that keep tests honest",[10,584,585,586,588],{},"A route that matches too loosely is a deleted assertion. ",[13,587,27],{}," gives four matchers worth using deliberately.",[10,590,591,594,595,598,599,602],{},[41,592,593],{},"Method and URL"," are the base match, and the URL can be a string, a pattern, or a host plus path. Prefer the most specific form the test can support: ",[13,596,597],{},"respx.get(\"https:\u002F\u002Fapi.example.com\u002Forders\u002F7\")"," fails when the code builds the wrong id, where a regex over ",[13,600,601],{},"\u002Forders\u002F.*"," does not.",[10,604,605,608,609,612],{},[41,606,607],{},"Query parameters"," with ",[13,610,611],{},"params="," match independently of ordering, which matters because dict ordering is an implementation detail of the calling code.",[10,614,615,618,619,44,622,625,626,629],{},[41,616,617],{},"Body matchers"," — ",[13,620,621],{},"json__eq",[13,623,624],{},"data__contains"," — pin what was sent. For anything beyond a simple equality, prefer asserting on ",[13,627,628],{},"route.calls.last.request.content"," afterwards, which produces a readable diff on failure.",[10,631,632,608,635,638],{},[41,633,634],{},"Headers",[13,636,637],{},"headers="," check only the named headers, so authentication can be asserted without freezing every default the client adds.",[70,640,642],{"className":72,"code":641,"language":74,"meta":75,"style":75},"route = respx.post(\n    \"https:\u002F\u002Fapi.example.com\u002Fcharges\",\n    json__eq={\"amount\": 500, \"currency\": \"eur\"},     # exact body\n    headers={\"Idempotency-Key\": \"order-7\"},          # only this header\n)\n",[13,643,644,649,654,659,664],{"__ignoreMap":75},[79,645,646],{"class":81,"line":82},[79,647,648],{},"route = respx.post(\n",[79,650,651],{"class":81,"line":88},[79,652,653],{},"    \"https:\u002F\u002Fapi.example.com\u002Fcharges\",\n",[79,655,656],{"class":81,"line":94},[79,657,658],{},"    json__eq={\"amount\": 500, \"currency\": \"eur\"},     # exact body\n",[79,660,661],{"class":81,"line":100},[79,662,663],{},"    headers={\"Idempotency-Key\": \"order-7\"},          # only this header\n",[79,665,666],{"class":81,"line":107},[79,667,668],{},")\n",[10,670,671,672,675,676,679],{},"Two global settings shape the default strictness, and both are worth setting explicitly in a shared fixture. ",[13,673,674],{},"assert_all_mocked=True"," (the default) makes an unmatched request an error rather than a passthrough, and ",[13,677,678],{},"assert_all_called=True"," (also the default inside the decorator) fails the test when a declared route was never used — which catches dead code paths and stale stubs left behind by a refactor.",[70,681,683],{"className":72,"code":682,"language":74,"meta":75,"style":75},"import pytest, respx\n\n@pytest.fixture\ndef api_mock():\n    with respx.mock(base_url=\"https:\u002F\u002Fapi.example.com\",\n                    assert_all_called=True) as router:\n        yield router                    # routes declared per test, all must fire\n",[13,684,685,690,694,699,704,709,714],{"__ignoreMap":75},[79,686,687],{"class":81,"line":82},[79,688,689],{},"import pytest, respx\n",[79,691,692],{"class":81,"line":88},[79,693,104],{"emptyLinePlaceholder":103},[79,695,696],{"class":81,"line":94},[79,697,698],{},"@pytest.fixture\n",[79,700,701],{"class":81,"line":100},[79,702,703],{},"def api_mock():\n",[79,705,706],{"class":81,"line":107},[79,707,708],{},"    with respx.mock(base_url=\"https:\u002F\u002Fapi.example.com\",\n",[79,710,711],{"class":81,"line":113},[79,712,713],{},"                    assert_all_called=True) as router:\n",[79,715,716],{"class":81,"line":119},[79,717,718],{},"        yield router                    # routes declared per test, all must fire\n",[30,720,722],{"id":721},"migrating-from-patched-clients","Migrating from patched clients",[10,724,725],{},"Most codebases arrive here with tests that patch the client object. Converting them is mechanical and each conversion strengthens the test.",[10,727,728,729,732,733,736],{},"Start by deleting the patch and adding ",[13,730,731],{},"@respx.mock",". The test will fail with ",[13,734,735],{},"AllMockedAssertionError"," naming the exact request the code made — which is immediately useful, because that request is the contract the old test never checked. Turn it into a route, and the test passes again, now asserting the URL and method for the first time.",[10,738,739,740,743,744,747,748,752,753,756],{},"Then replace ",[13,741,742],{},"mock_client.get.assert_called_once_with(...)"," with assertions on ",[13,745,746],{},"route.calls.last.request",". The old assertion checked the arguments ",[749,750,751],"em",{},"your code passed to the client","; the new one checks the request ",[749,754,755],{},"the client actually built",", which is a stronger claim and catches base-URL and encoding bugs that the argument-level assertion cannot see.",[10,758,759,760,763,764,44,767,770,771,774,775,778],{},"Finally, delete any fake response objects the old test constructed by hand. ",[13,761,762],{},"httpx.Response(200, json=...)"," builds a real one, so downstream code that calls ",[13,765,766],{},".json()",[13,768,769],{},".raise_for_status()"," or reads ",[13,772,773],{},".headers"," runs against the genuine implementation instead of a ",[13,776,777],{},"Mock"," that returns whatever it was told to.",[30,780,782],{"id":781},"edge-cases-and-failure-modes","Edge cases and failure modes",[35,784,785,798,811,823,836,851],{},[38,786,787,790,791,794,795,61],{},[41,788,789],{},"Event hooks and retries run for real."," A client configured with ",[13,792,793],{},"transport=httpx.AsyncHTTPTransport(retries=3)"," retries against the mock too, which is usually desirable but doubles the expected ",[13,796,797],{},"call_count",[38,799,800,806,807,810],{},[41,801,802,803,61],{},"Streaming responses need ",[13,804,805],{},"httpx.Response(200, stream=...)"," Returning a plain body and then reading ",[13,808,809],{},".aiter_bytes()"," works, but tests of chunked handling should build the stream explicitly.",[38,812,813,819,820,822],{},[41,814,815,818],{},[13,816,817],{},"base_url"," on the router and on the client compose."," Declaring a route with a full URL while the router also has a ",[13,821,817],{}," produces a match failure that reads confusingly; pick one place to put the host.",[38,824,825,828,829,832,833,835],{},[41,826,827],{},"Unmatched requests raise inside the client",", so a ",[13,830,831],{},"try\u002Fexcept httpx.HTTPError"," in the code under test can swallow the mocking error. Assert ",[13,834,216],{}," explicitly rather than relying on the exception surfacing.",[38,837,838,846,847,61],{},[41,839,840,220,842,845],{},[13,841,27],{},[13,843,844],{},"pytest-asyncio"," fixture scopes interact."," A router created in a session-scoped fixture outlives the event loop that tests run on; keep the router function-scoped, as discussed in ",[57,848,850],{"href":849},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fhow-to-scope-pytest-fixtures-for-async-tests\u002F","how to scope pytest fixtures for async tests",[38,852,853,856,857,860,861,864],{},[41,854,855],{},"Not every library uses httpx's transport."," A vendor SDK built on ",[13,858,859],{},"aiohttp"," or raw ",[13,862,863],{},"urllib3"," is unaffected by respx and needs its own interception layer.",[30,866,868],{"id":867},"sharing-routes-across-a-test-suite","Sharing routes across a test suite",[10,870,871],{},"Once more than a handful of tests talk to the same API, the route definitions want to live in one place — otherwise every test re-declares the happy path and a change to the API contract means editing thirty files.",[10,873,874],{},"A fixture that installs the baseline routes and yields the router gives each test a working default it can override:",[70,876,878],{"className":72,"code":877,"language":74,"meta":75,"style":75},"import httpx, pytest, respx\n\n@pytest.fixture\ndef api(respx_mock):                      # respx ships a pytest fixture\n    respx_mock.get(\"\u002Fhealth\").mock(return_value=httpx.Response(200))\n    respx_mock.get(\"\u002Forders\").mock(return_value=httpx.Response(200, json={\"orders\": []}))\n    return respx_mock\n\ndef test_specific_case(api):\n    # Re-declaring a route replaces the earlier one for this test only.\n    api.get(\"\u002Forders\").mock(return_value=httpx.Response(503))\n    ...\n",[13,879,880,885,889,893,898,903,908,913,917,922,927,932],{"__ignoreMap":75},[79,881,882],{"class":81,"line":82},[79,883,884],{},"import httpx, pytest, respx\n",[79,886,887],{"class":81,"line":88},[79,888,104],{"emptyLinePlaceholder":103},[79,890,891],{"class":81,"line":94},[79,892,698],{},[79,894,895],{"class":81,"line":100},[79,896,897],{},"def api(respx_mock):                      # respx ships a pytest fixture\n",[79,899,900],{"class":81,"line":107},[79,901,902],{},"    respx_mock.get(\"\u002Fhealth\").mock(return_value=httpx.Response(200))\n",[79,904,905],{"class":81,"line":113},[79,906,907],{},"    respx_mock.get(\"\u002Forders\").mock(return_value=httpx.Response(200, json={\"orders\": []}))\n",[79,909,910],{"class":81,"line":119},[79,911,912],{},"    return respx_mock\n",[79,914,915],{"class":81,"line":125},[79,916,104],{"emptyLinePlaceholder":103},[79,918,919],{"class":81,"line":131},[79,920,921],{},"def test_specific_case(api):\n",[79,923,924],{"class":81,"line":137},[79,925,926],{},"    # Re-declaring a route replaces the earlier one for this test only.\n",[79,928,929],{"class":81,"line":143},[79,930,931],{},"    api.get(\"\u002Forders\").mock(return_value=httpx.Response(503))\n",[79,933,934],{"class":81,"line":149},[79,935,936],{},"    ...\n",[10,938,939,940,943],{},"Two conventions keep that from becoming its own maintenance problem. Keep the shared fixture to responses that are genuinely uninteresting — health checks, token endpoints, anything every test needs and no test asserts on. And keep ",[13,941,942],{},"assert_all_called"," off for the shared routes, since not every test will exercise them; enable it per test for the routes that test declares itself.",[10,945,946],{},"The larger version of this problem — keeping the mocked responses faithful to the real API — is what contract testing and recorded cassettes address, and it is worth deciding early which of the two the project will rely on rather than discovering the drift in production.",[312,948,950,1041],{"className":949},[315],[317,951,325,956,325,959,325,962,325,964,325,966,325,971,325,975,325,979,325,983,325,987,325,992,325,994,325,997,325,1000,325,1004,325,1006,325,1009,325,1012,325,1016,325,1018,325,1021,325,1024,325,1028,325,1031,325,1034,325,1038],{"viewBox":952,"role":320,"ariaLabelledBy":953,"xmlns":324},"0 0 760 270",[954,955],"respxassert-t","respxassert-d",[327,957,958],{"id":954},"What to assert, and where it lives",[331,960,961],{"id":955},"A table of four things a test may want to assert about an HTTP interaction - that the call happened, how many times, what was sent, and that nothing else was called - naming the respx attribute for each.",[335,963],{"x":337,"y":337,"width":338,"height":507,"rx":340,"fill":341},[343,965,958],{"x":345,"y":346,"textAnchor":347,"fontSize":348,"fontWeight":349,"fill":350},[335,967],{"x":968,"y":354,"width":969,"height":492,"rx":355,"fill":377,"stroke":350,"strokeWidth":970},"24","712","1.5",[343,972,974],{"x":973,"y":496,"fontSize":497,"fontWeight":349,"fill":350},"38","Criterion",[343,976,978],{"x":977,"y":496,"textAnchor":347,"fontSize":497,"fontWeight":349,"fill":350},"390","Assert with",[343,980,982],{"x":981,"y":496,"textAnchor":347,"fontSize":497,"fontWeight":349,"fill":350},"620","Catches",[335,984],{"x":968,"y":985,"width":969,"height":492,"fill":356,"stroke":350,"strokeWidth":986},"92","1",[343,988,991],{"x":973,"y":989,"fontSize":990,"fill":350},"116","11.5","the call happened",[343,993,216],{"x":977,"y":989,"textAnchor":347,"fontSize":990,"fill":350},[343,995,996],{"x":981,"y":989,"textAnchor":347,"fontSize":990,"fill":350},"wrong URL or method",[335,998],{"x":968,"y":999,"width":969,"height":492,"fill":341,"stroke":350,"strokeWidth":986},"132",[343,1001,1003],{"x":973,"y":1002,"fontSize":990,"fill":350},"156","how many times",[343,1005,219],{"x":977,"y":1002,"textAnchor":347,"fontSize":990,"fill":350},[343,1007,1008],{"x":981,"y":1002,"textAnchor":347,"fontSize":990,"fill":350},"missing or extra retries",[335,1010],{"x":968,"y":1011,"width":969,"height":492,"fill":356,"stroke":350,"strokeWidth":986},"172",[343,1013,1015],{"x":973,"y":1014,"fontSize":990,"fill":350},"196","what was sent",[343,1017,746],{"x":977,"y":1014,"textAnchor":347,"fontSize":990,"fill":350},[343,1019,1020],{"x":981,"y":1014,"textAnchor":347,"fontSize":990,"fill":350},"payload and header bugs",[335,1022],{"x":968,"y":1023,"width":969,"height":492,"fill":341,"stroke":350,"strokeWidth":986},"212",[343,1025,1027],{"x":973,"y":1026,"fontSize":990,"fill":350},"236","nothing else",[343,1029,1030],{"x":977,"y":1026,"textAnchor":347,"fontSize":990,"fill":350},"assert_all_mocked",[343,1032,1033],{"x":981,"y":1026,"textAnchor":347,"fontSize":990,"fill":350},"unexpected requests",[81,1035],{"x1":1036,"y1":354,"x2":1036,"y2":1037,"stroke":350,"strokeWidth":986},"274","252",[81,1039],{"x1":1040,"y1":354,"x2":1040,"y2":1037,"stroke":350,"strokeWidth":986},"505",[418,1042,1043],{},"The last row is the one teams forget, and it is the one that turns an unexpected request into a failure rather than a passthrough.",[30,1045,1047],{"id":1046},"frequently-asked-questions","Frequently Asked Questions",[10,1049,1050,1053,1054,220,1057,1060],{},[41,1051,1052],{},"Does respx work with both sync and async httpx clients?","\nYes. respx patches the httpx transport layer, which both client types share, so the same route definitions serve ",[13,1055,1056],{},"AsyncClient",[13,1058,1059],{},"Client"," without any change to the test.",[10,1062,1063,1066,1067,1069,1070,1073],{},[41,1064,1065],{},"What happens to a request that matches no route?","\nBy default respx raises ",[13,1068,735],{},", so an unexpected request fails the test rather than escaping to the network. Pass ",[13,1071,1072],{},"assert_all_mocked=False"," to let unmatched requests pass through instead.",[10,1075,1076,1079,1080,1082,1083,1085],{},[41,1077,1078],{},"How do I assert a request was made with a specific body?","\nEvery route records its calls, so ",[13,1081,746],{}," gives the ",[13,1084,227],{}," object with its content, headers and URL. Assert on that rather than trying to express everything in the route matcher.",[10,1087,1088,1091,1092,1095],{},[41,1089,1090],{},"Can respx simulate timeouts and connection errors?","\nYes. A route can be given ",[13,1093,1094],{},"side_effect=httpx.ConnectTimeout"," or any exception class, which raises when the route matches — the standard way to test retry and timeout handling without waiting.",[30,1097,1099],{"id":1098},"related","Related",[35,1101,1102,1108,1119,1126],{},[38,1103,1104,1107],{},[57,1105,1106],{"href":59},"Mocking network and HTTP calls"," — the interception layers and when each is right.",[38,1109,1110,1114,1115,1118],{},[57,1111,1113],{"href":1112},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-requests-with-the-responses-library\u002F","Mocking requests with the responses library"," — the same approach for the ",[13,1116,1117],{},"requests"," stack.",[38,1120,1121,1125],{},[57,1122,1124],{"href":1123},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Frecording-and-replaying-http-with-vcrpy\u002F","Recording and replaying HTTP with VCR.py"," — when the response bodies should come from the real API.",[38,1127,1128,1132],{},[57,1129,1131],{"href":1130},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002Fmock-vs-magicmock-vs-asyncmock-when-to-use-each\u002F","Mock vs MagicMock vs AsyncMock — when to use each"," — the double you no longer need once the transport is mocked.",[10,1134,1135,1136],{},"← Back to ",[57,1137,1138],{"href":59},"Mocking Network and HTTP Calls",[1140,1141,1142],"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":75,"searchDepth":88,"depth":88,"links":1144},[1145,1146,1147,1148,1149,1150,1151,1152,1153],{"id":32,"depth":88,"text":33},{"id":64,"depth":88,"text":65},{"id":423,"depth":88,"text":424},{"id":581,"depth":88,"text":582},{"id":721,"depth":88,"text":722},{"id":781,"depth":88,"text":782},{"id":867,"depth":88,"text":868},{"id":1046,"depth":88,"text":1047},{"id":1098,"depth":88,"text":1099},"Intercept httpx requests with respx: routing by method and URL, async client support, asserting call counts, and why transport-level mocking beats patching the client.","md",{"slug":1157,"type":1158,"breadcrumb":1159,"datePublished":1160,"dateModified":1160,"faq":1161,"howto":1170},"mocking-httpx-clients-with-respx","article","respx for httpx","2026-08-01",[1162,1164,1166,1168],{"q":1052,"a":1163},"Yes. respx patches the httpx transport layer, which both client types share, so the same route definitions serve AsyncClient and Client without any change to the test.",{"q":1065,"a":1165},"By default respx raises AllMockedAssertionError, so an unexpected request fails the test rather than escaping to the network. Pass assert_all_mocked=False to let unmatched requests pass through instead.",{"q":1078,"a":1167},"Every route records its calls, so route.calls.last.request gives the httpx Request object with its content, headers and URL. Assert on that rather than trying to express everything in the route matcher.",{"q":1090,"a":1169},"Yes. A route can be given side_effect=httpx.ConnectTimeout or any exception class, which raises when the route matches — the standard way to test retry and timeout handling without waiting.",{"name":1171,"description":1172,"steps":1173},"How to mock httpx requests with respx","Route httpx traffic to declared responses and assert on what the client actually sent.",[1174,1177,1180,1183],{"name":1175,"text":1176},"Activate the mock router","Use the respx.mock decorator or context manager so the transport is intercepted for the duration.",{"name":1178,"text":1179},"Declare routes for the requests you expect","Match on method and URL, adding body, header or query matchers where the request shape matters.",{"name":1181,"text":1182},"Return responses or raise errors","Give each route an httpx.Response, or a side effect exception to simulate a failure.",{"name":1184,"text":1185},"Assert on the recorded calls","Read route.called, route.call_count and route.calls to verify what the client sent.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-httpx-clients-with-respx",{"title":5,"description":1154},"advanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-httpx-clients-with-respx\u002Findex","hhR1n_L_L3gN7QuPyPFLwd43PUjm82UZ-zijdDndVb0",1785613404424]