[{"data":1,"prerenderedAt":1402},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002F":3},{"id":4,"title":5,"body":6,"description":1363,"extension":1364,"meta":1365,"navigation":458,"path":1398,"seo":1399,"stem":1400,"__hash__":1401},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Findex.md","Mocking Network and HTTP Calls",{"type":7,"value":8,"toc":1347},"minimark",[9,26,31,78,136,150,154,196,340,359,363,368,379,401,408,412,433,577,600,604,622,708,723,727,746,861,878,882,893,941,949,953,956,998,1005,1009,1012,1072,1076,1216,1220,1232,1252,1261,1286,1290,1336,1343],[10,11,12,13,17,18,21,22,25],"p",{},"A unit test that reaches a live HTTP endpoint is not a unit test; it is a flaky integration test wearing a disguise. It fails when the network blips, when the upstream rate-limits CI, or when a colleague's branch mutates shared staging data — and it fails non-deterministically, which is the most expensive kind of failure to debug. The symptom is a test that is green on your laptop and red on the third parallel CI shard. The fix is to intercept outbound calls at a well-chosen layer and replay deterministic responses, while still driving your real client so that retries, header handling, and JSON decoding execute exactly as they will in production. This guide maps the full spectrum from naive ",[14,15,16],"code",{},"monkeypatch"," through the ",[14,19,20],{},"responses"," and ",[14,23,24],{},"respx"," interception libraries to a real fake server, and shows where each one is the right tool.",[27,28,30],"h2",{"id":29},"prerequisites","Prerequisites",[32,33,34,41,46,56,69],"ul",{},[35,36,37,40],"li",{},[14,38,39],{},"python >= 3.9",".",[35,42,43,40],{},[14,44,45],{},"pytest >= 8.0",[35,47,48,49,52,53,40],{},"HTTP clients: ",[14,50,51],{},"requests >= 2.31"," and\u002For ",[14,54,55],{},"httpx >= 0.27",[35,57,58,59,62,63,62,66,40],{},"Interception libraries, installed as needed: ",[14,60,61],{},"responses >= 0.25",", ",[14,64,65],{},"respx >= 0.21",[14,67,68],{},"requests-mock >= 1.12",[35,70,71,72,62,75,40],{},"Socket guards and fake servers: ",[14,73,74],{},"pytest-socket >= 0.7",[14,76,77],{},"pytest-httpserver >= 1.1",[79,80,85],"pre",{"className":81,"code":82,"language":83,"meta":84,"style":84},"language-bash shiki shiki-themes github-light github-dark","pip install \"pytest>=8.0\" \"requests>=2.31\" \"httpx>=0.27\" \\\n            \"responses>=0.25\" \"respx>=0.21\" \"requests-mock>=1.12\" \\\n            \"pytest-socket>=0.7\" \"pytest-httpserver>=1.1\"\n","bash","",[14,86,87,113,127],{"__ignoreMap":84},[88,89,92,96,100,103,106,109],"span",{"class":90,"line":91},"line",1,[88,93,95],{"class":94},"sScJk","pip",[88,97,99],{"class":98},"sZZnC"," install",[88,101,102],{"class":98}," \"pytest>=8.0\"",[88,104,105],{"class":98}," \"requests>=2.31\"",[88,107,108],{"class":98}," \"httpx>=0.27\"",[88,110,112],{"class":111},"sj4cs"," \\\n",[88,114,116,119,122,125],{"class":90,"line":115},2,[88,117,118],{"class":98},"            \"responses>=0.25\"",[88,120,121],{"class":98}," \"respx>=0.21\"",[88,123,124],{"class":98}," \"requests-mock>=1.12\"",[88,126,112],{"class":111},[88,128,130,133],{"class":90,"line":129},3,[88,131,132],{"class":98},"            \"pytest-socket>=0.7\"",[88,134,135],{"class":98}," \"pytest-httpserver>=1.1\"\n",[10,137,138,139,144,145,149],{},"The interception techniques here build on the namespace rules in ",[140,141,143],"a",{"href":142},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002F","Patching Strategies for Complex Codebases"," and the spec discipline from ",[140,146,148],{"href":147},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002F","Autospec & Strict Mocking"," — both matter the moment you stop hand-rolling fake responses.",[27,151,153],{"id":152},"core-concept","Core concept",[10,155,156,157,62,160,163,164,167,168,172,173,176,177,180,181,62,184,187,188,191,192,195],{},"Every HTTP call descends through a stack: your application function calls a high-level client method (",[14,158,159],{},"requests.get",[14,161,162],{},"httpx.Client.get","), which builds a ",[14,165,166],{},"Request",", hands it to a ",[169,170,171],"strong",{},"transport adapter"," (",[14,174,175],{},"requests","' ",[14,178,179],{},"HTTPAdapter"," over ",[14,182,183],{},"urllib3",[14,185,186],{},"httpx","'s ",[14,189,190],{},"HTTPTransport","), which finally opens a ",[169,193,194],{},"socket",". You can substitute a double at any layer, and the layer you choose determines both how realistic the test is and how brittle it is.",[197,198,201,336],"figure",{"className":199},[200],"diagram",[202,203,210,211,210,215,210,219,210,229,210,236,210,240,210,246,210,250,210,258,210,264,210,269,210,273,210,277,210,281,210,284,210,287,210,290,210,294,210,297,210,300,210,303,210,307,210,310,210,313,210,320,210,324,210,328,210,332],"svg",{"viewBox":204,"role":205,"ariaLabelledBy":206,"xmlns":209},"0 0 820 360","img",[207,208],"httpmock-t","httpmock-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[212,213,214],"title",{"id":207},"HTTP test double spectrum",[216,217,218],"desc",{"id":208},"A spectrum from a real server through a local fake server and transport interception to a hand-rolled client mock, trading fidelity against speed and brittleness.",[220,221,228],"text",{"x":222,"y":223,"textAnchor":224,"fontSize":225,"fontWeight":226,"fill":227},"410","34","middle","19","700","#3d405b","Where to intercept an HTTP call",[220,230,235],{"x":231,"y":232,"textAnchor":224,"fontSize":233,"fontWeight":226,"fill":234},"120","66","12","#81b29a","high fidelity",[220,237,239],{"x":226,"y":232,"textAnchor":224,"fontSize":233,"fontWeight":226,"fill":238},"#e07a5f","low fidelity, fast",[90,241],{"x1":242,"y1":243,"x2":244,"y2":243,"stroke":227,"strokeWidth":245},"40","78","780","1.5",[247,248],"polygon",{"points":249,"fill":227},"780,78 770,73 770,83",[251,252],"rect",{"x":242,"y":253,"width":254,"height":255,"rx":233,"fill":256,"stroke":234,"strokeWidth":257},"96","170","92","#fffdf8","2",[220,259,263],{"x":260,"y":261,"textAnchor":224,"fontSize":262,"fontWeight":226,"fill":227},"125","128","13","Real \u002F fake server",[220,265,268],{"x":260,"y":266,"textAnchor":224,"fontSize":267,"fill":227},"150","11","pytest-httpserver",[220,270,272],{"x":260,"y":271,"textAnchor":224,"fontSize":267,"fill":227},"168","real socket, loopback",[251,274],{"x":275,"y":253,"width":254,"height":255,"rx":233,"fill":256,"stroke":276,"strokeWidth":257},"230","#f2cc8f",[220,278,280],{"x":279,"y":261,"textAnchor":224,"fontSize":262,"fontWeight":226,"fill":227},"315","Transport stub",[220,282,283],{"x":279,"y":266,"textAnchor":224,"fontSize":267,"fill":227},"responses, respx",[220,285,286],{"x":279,"y":271,"textAnchor":224,"fontSize":267,"fill":227},"requests-mock",[251,288],{"x":289,"y":253,"width":254,"height":255,"rx":233,"fill":256,"stroke":276,"strokeWidth":257},"420",[220,291,293],{"x":292,"y":261,"textAnchor":224,"fontSize":262,"fontWeight":226,"fill":227},"505","Client method patch",[220,295,296],{"x":292,"y":266,"textAnchor":224,"fontSize":267,"fill":227},"monkeypatch get\u002Fpost",[220,298,299],{"x":292,"y":271,"textAnchor":224,"fontSize":267,"fill":227},"returns Response",[251,301],{"x":302,"y":253,"width":254,"height":255,"rx":233,"fill":256,"stroke":238,"strokeWidth":257},"610",[220,304,306],{"x":305,"y":261,"textAnchor":224,"fontSize":262,"fontWeight":226,"fill":227},"695","Hand-rolled mock",[220,308,309],{"x":305,"y":266,"textAnchor":224,"fontSize":267,"fill":227},"MagicMock dict",[220,311,312],{"x":305,"y":271,"textAnchor":224,"fontSize":267,"fill":227},"skips parsing",[251,314],{"x":242,"y":315,"width":316,"height":317,"rx":318,"fill":319,"stroke":227,"strokeWidth":245},"222","740","116","14","#f4f1de",[220,321,323],{"x":222,"y":322,"textAnchor":224,"fontSize":262,"fontWeight":226,"fill":227},"250","Rule of thumb",[220,325,327],{"x":222,"y":326,"textAnchor":224,"fontSize":233,"fill":227},"276","Transport stubs (responses \u002F respx) are the default sweet spot:",[220,329,331],{"x":222,"y":330,"textAnchor":224,"fontSize":233,"fill":227},"296","real client code runs, no socket opens.",[220,333,335],{"x":222,"y":334,"textAnchor":224,"fontSize":233,"fill":227},"320","Move left for transport fidelity, right only for trivial one-offs.",[337,338,339],"figcaption",{},"The interception layer is a fidelity-versus-brittleness trade-off: a fake server exercises the real transport, transport stubs keep your client code real while replaying canned bytes, and a hand-rolled client mock is fast but bypasses status, headers, and decoding.",[10,341,342,343,346,347,350,351,354,355,358],{},"The pattern to internalize: ",[169,344,345],{},"intercept as low in the stack as you can afford to",", because every layer you skip is a layer of production behaviour the test no longer covers. A hand-rolled ",[14,348,349],{},"MagicMock"," that returns ",[14,352,353],{},"{\"id\": 1}"," never exercises status-code checks or ",[14,356,357],{},"raise_for_status()","; a transport stub does.",[27,360,362],{"id":361},"step-by-step-implementation","Step-by-step implementation",[364,365,367],"h3",{"id":366},"_1-block-real-sockets-so-mistakes-fail-loudly","1. Block real sockets so mistakes fail loudly",[10,369,370,371,374,375,378],{},"Before adding a single stub, make unmocked calls impossible. ",[14,372,373],{},"pytest-socket"," disables socket creation suite-wide and raises ",[14,376,377],{},"SocketBlockedError"," on any escape.",[79,380,384],{"className":381,"code":382,"language":383,"meta":84,"style":84},"language-python shiki shiki-themes github-light github-dark","# pyproject.toml\n[tool.pytest.ini_options]\naddopts = \"--disable-socket --allow-hosts=127.0.0.1,::1\"\n","python",[14,385,386,391,396],{"__ignoreMap":84},[88,387,388],{"class":90,"line":91},[88,389,390],{},"# pyproject.toml\n",[88,392,393],{"class":90,"line":115},[88,394,395],{},"[tool.pytest.ini_options]\n",[88,397,398],{"class":90,"line":129},[88,399,400],{},"addopts = \"--disable-socket --allow-hosts=127.0.0.1,::1\"\n",[10,402,403,404,407],{},"With this in place a test that forgets to register a stub fails immediately with a clear message instead of hanging on a DNS timeout or — worse — quietly mutating a real service. The ",[14,405,406],{},"--allow-hosts"," whitelist keeps loopback open so a local fake server still works.",[364,409,411],{"id":410},"_2-the-naive-baseline-monkeypatch-the-client-method","2. The naive baseline: monkeypatch the client method",[10,413,414,415,418,419,422,423,62,426,429,430,432],{},"The cheapest double replaces the client function and returns a real ",[14,416,417],{},"Response",". Build a genuine ",[14,420,421],{},"requests.Response"," so ",[14,424,425],{},"status_code",[14,427,428],{},".json()",", and ",[14,431,357],{}," all behave.",[79,434,436],{"className":381,"code":435,"language":383,"meta":84,"style":84},"import json\nimport requests\nfrom myapp.client import fetch_user  # calls requests.get(...).json()\n\ndef _make_response(payload: dict, status: int = 200) -> requests.Response:\n    resp = requests.Response()\n    resp.status_code = status\n    # _content must be bytes; .json() decodes it through the real machinery.\n    resp._content = json.dumps(payload).encode(\"utf-8\")\n    resp.headers[\"Content-Type\"] = \"application\u002Fjson\"\n    return resp\n\ndef test_fetch_user_monkeypatch(monkeypatch):\n    captured = {}\n    def fake_get(url, **kwargs):\n        captured[\"url\"] = url            # capture so we can assert the call\n        return _make_response({\"id\": 7, \"name\": \"Ada\"})\n    # Patch where requests.get is LOOKED UP, not where it is defined.\n    monkeypatch.setattr(\"myapp.client.requests.get\", fake_get)\n\n    user = fetch_user(7)\n\n    assert user[\"name\"] == \"Ada\"\n    assert captured[\"url\"].endswith(\"\u002Fusers\u002F7\")\n",[14,437,438,443,448,453,460,466,472,478,484,490,496,502,507,513,519,525,531,537,543,549,554,560,565,571],{"__ignoreMap":84},[88,439,440],{"class":90,"line":91},[88,441,442],{},"import json\n",[88,444,445],{"class":90,"line":115},[88,446,447],{},"import requests\n",[88,449,450],{"class":90,"line":129},[88,451,452],{},"from myapp.client import fetch_user  # calls requests.get(...).json()\n",[88,454,456],{"class":90,"line":455},4,[88,457,459],{"emptyLinePlaceholder":458},true,"\n",[88,461,463],{"class":90,"line":462},5,[88,464,465],{},"def _make_response(payload: dict, status: int = 200) -> requests.Response:\n",[88,467,469],{"class":90,"line":468},6,[88,470,471],{},"    resp = requests.Response()\n",[88,473,475],{"class":90,"line":474},7,[88,476,477],{},"    resp.status_code = status\n",[88,479,481],{"class":90,"line":480},8,[88,482,483],{},"    # _content must be bytes; .json() decodes it through the real machinery.\n",[88,485,487],{"class":90,"line":486},9,[88,488,489],{},"    resp._content = json.dumps(payload).encode(\"utf-8\")\n",[88,491,493],{"class":90,"line":492},10,[88,494,495],{},"    resp.headers[\"Content-Type\"] = \"application\u002Fjson\"\n",[88,497,499],{"class":90,"line":498},11,[88,500,501],{},"    return resp\n",[88,503,505],{"class":90,"line":504},12,[88,506,459],{"emptyLinePlaceholder":458},[88,508,510],{"class":90,"line":509},13,[88,511,512],{},"def test_fetch_user_monkeypatch(monkeypatch):\n",[88,514,516],{"class":90,"line":515},14,[88,517,518],{},"    captured = {}\n",[88,520,522],{"class":90,"line":521},15,[88,523,524],{},"    def fake_get(url, **kwargs):\n",[88,526,528],{"class":90,"line":527},16,[88,529,530],{},"        captured[\"url\"] = url            # capture so we can assert the call\n",[88,532,534],{"class":90,"line":533},17,[88,535,536],{},"        return _make_response({\"id\": 7, \"name\": \"Ada\"})\n",[88,538,540],{"class":90,"line":539},18,[88,541,542],{},"    # Patch where requests.get is LOOKED UP, not where it is defined.\n",[88,544,546],{"class":90,"line":545},19,[88,547,548],{},"    monkeypatch.setattr(\"myapp.client.requests.get\", fake_get)\n",[88,550,552],{"class":90,"line":551},20,[88,553,459],{"emptyLinePlaceholder":458},[88,555,557],{"class":90,"line":556},21,[88,558,559],{},"    user = fetch_user(7)\n",[88,561,563],{"class":90,"line":562},22,[88,564,459],{"emptyLinePlaceholder":458},[88,566,568],{"class":90,"line":567},23,[88,569,570],{},"    assert user[\"name\"] == \"Ada\"\n",[88,572,574],{"class":90,"line":573},24,[88,575,576],{},"    assert captured[\"url\"].endswith(\"\u002Fusers\u002F7\")\n",[10,578,579,580,584,585,588,589,591,592,599],{},"The hard part is the ",[581,582,583],"em",{},"target",": you patch ",[14,586,587],{},"myapp.client.requests.get",", the name as the code under test resolves it, not ",[14,590,159],{},". That namespace rule is the single most common cause of a green test over live code. The ",[140,593,595,596,598],{"href":594},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002F","pytest ",[14,597,16],{}," fixture"," auto-reverts on teardown, so no manual cleanup is needed. This approach stops scaling the moment you need URL matching, multiple sequenced responses, or call assertions — that is where dedicated libraries earn their place.",[364,601,603],{"id":602},"_3-the-default-tool-for-requests-the-responses-library","3. The default tool for requests: the responses library",[10,605,606,608,609,611,612,614,615,617,618,621],{},[14,607,20],{}," patches ",[14,610,175],{}," at the ",[14,613,179],{},"\u002F",[14,616,183],{}," boundary, so your real ",[14,619,620],{},"Session",", retry config, and JSON decoding all run. You declare expectations; it replays them and records calls.",[79,623,625],{"className":381,"code":624,"language":383,"meta":84,"style":84},"import responses\nimport requests\nfrom myapp.client import fetch_user\n\n@responses.activate\ndef test_fetch_user_with_responses():\n    responses.add(\n        responses.GET,\n        \"https:\u002F\u002Fapi.example.com\u002Fusers\u002F7\",\n        json={\"id\": 7, \"name\": \"Ada\"},   # serialized + Content-Type set for you\n        status=200,\n    )\n    user = fetch_user(7)\n    assert user[\"name\"] == \"Ada\"\n    # Records every intercepted call for assertions.\n    assert len(responses.calls) == 1\n    assert responses.calls[0].request.headers[\"Accept\"] == \"application\u002Fjson\"\n",[14,626,627,632,636,641,645,650,655,660,665,670,675,680,685,689,693,698,703],{"__ignoreMap":84},[88,628,629],{"class":90,"line":91},[88,630,631],{},"import responses\n",[88,633,634],{"class":90,"line":115},[88,635,447],{},[88,637,638],{"class":90,"line":129},[88,639,640],{},"from myapp.client import fetch_user\n",[88,642,643],{"class":90,"line":455},[88,644,459],{"emptyLinePlaceholder":458},[88,646,647],{"class":90,"line":462},[88,648,649],{},"@responses.activate\n",[88,651,652],{"class":90,"line":468},[88,653,654],{},"def test_fetch_user_with_responses():\n",[88,656,657],{"class":90,"line":474},[88,658,659],{},"    responses.add(\n",[88,661,662],{"class":90,"line":480},[88,663,664],{},"        responses.GET,\n",[88,666,667],{"class":90,"line":486},[88,668,669],{},"        \"https:\u002F\u002Fapi.example.com\u002Fusers\u002F7\",\n",[88,671,672],{"class":90,"line":492},[88,673,674],{},"        json={\"id\": 7, \"name\": \"Ada\"},   # serialized + Content-Type set for you\n",[88,676,677],{"class":90,"line":498},[88,678,679],{},"        status=200,\n",[88,681,682],{"class":90,"line":504},[88,683,684],{},"    )\n",[88,686,687],{"class":90,"line":509},[88,688,559],{},[88,690,691],{"class":90,"line":515},[88,692,570],{},[88,694,695],{"class":90,"line":521},[88,696,697],{},"    # Records every intercepted call for assertions.\n",[88,699,700],{"class":90,"line":527},[88,701,702],{},"    assert len(responses.calls) == 1\n",[88,704,705],{"class":90,"line":533},[88,706,707],{},"    assert responses.calls[0].request.headers[\"Accept\"] == \"application\u002Fjson\"\n",[10,709,710,711,714,715,718,719,40],{},"Sequenced responses (for retry\u002Fbackoff testing) are just repeated ",[14,712,713],{},"add"," calls to the same URL — the first matching unconsumed registration fires, in order. The full registry API, matchers, and ",[14,716,717],{},"assert_all_requests_are_fired"," are covered in ",[140,720,722],{"href":721},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-requests-with-the-responses-library\u002F","Mocking requests with the responses Library",[364,724,726],{"id":725},"_4-the-httpx-equivalent-respx","4. The httpx equivalent: respx",[10,728,729,731,732,734,735,737,738,741,742,745],{},[14,730,20],{}," does not see ",[14,733,186],{}," traffic. ",[14,736,24],{}," intercepts at httpx's transport layer and covers both sync ",[14,739,740],{},"httpx.Client"," and async ",[14,743,744],{},"httpx.AsyncClient"," with one API.",[79,747,749],{"className":381,"code":748,"language":383,"meta":84,"style":84},"import httpx\nimport respx\nimport pytest\n\n@respx.mock\ndef test_sync_httpx():\n    route = respx.get(\"https:\u002F\u002Fapi.example.com\u002Fusers\u002F7\").mock(\n        return_value=httpx.Response(200, json={\"id\": 7, \"name\": \"Ada\"})\n    )\n    with httpx.Client() as client:\n        resp = client.get(\"https:\u002F\u002Fapi.example.com\u002Fusers\u002F7\")\n    assert resp.json()[\"name\"] == \"Ada\"\n    assert route.called\n\n@pytest.mark.asyncio\n@respx.mock\nasync def test_async_httpx():\n    respx.get(\"https:\u002F\u002Fapi.example.com\u002Fping\").mock(\n        return_value=httpx.Response(204)\n    )\n    async with httpx.AsyncClient() as client:\n        resp = await client.get(\"https:\u002F\u002Fapi.example.com\u002Fping\")\n    assert resp.status_code == 204\n",[14,750,751,756,761,766,770,775,780,785,790,794,799,804,809,814,818,823,827,832,837,842,846,851,856],{"__ignoreMap":84},[88,752,753],{"class":90,"line":91},[88,754,755],{},"import httpx\n",[88,757,758],{"class":90,"line":115},[88,759,760],{},"import respx\n",[88,762,763],{"class":90,"line":129},[88,764,765],{},"import pytest\n",[88,767,768],{"class":90,"line":455},[88,769,459],{"emptyLinePlaceholder":458},[88,771,772],{"class":90,"line":462},[88,773,774],{},"@respx.mock\n",[88,776,777],{"class":90,"line":468},[88,778,779],{},"def test_sync_httpx():\n",[88,781,782],{"class":90,"line":474},[88,783,784],{},"    route = respx.get(\"https:\u002F\u002Fapi.example.com\u002Fusers\u002F7\").mock(\n",[88,786,787],{"class":90,"line":480},[88,788,789],{},"        return_value=httpx.Response(200, json={\"id\": 7, \"name\": \"Ada\"})\n",[88,791,792],{"class":90,"line":486},[88,793,684],{},[88,795,796],{"class":90,"line":492},[88,797,798],{},"    with httpx.Client() as client:\n",[88,800,801],{"class":90,"line":498},[88,802,803],{},"        resp = client.get(\"https:\u002F\u002Fapi.example.com\u002Fusers\u002F7\")\n",[88,805,806],{"class":90,"line":504},[88,807,808],{},"    assert resp.json()[\"name\"] == \"Ada\"\n",[88,810,811],{"class":90,"line":509},[88,812,813],{},"    assert route.called\n",[88,815,816],{"class":90,"line":515},[88,817,459],{"emptyLinePlaceholder":458},[88,819,820],{"class":90,"line":521},[88,821,822],{},"@pytest.mark.asyncio\n",[88,824,825],{"class":90,"line":527},[88,826,774],{},[88,828,829],{"class":90,"line":533},[88,830,831],{},"async def test_async_httpx():\n",[88,833,834],{"class":90,"line":539},[88,835,836],{},"    respx.get(\"https:\u002F\u002Fapi.example.com\u002Fping\").mock(\n",[88,838,839],{"class":90,"line":545},[88,840,841],{},"        return_value=httpx.Response(204)\n",[88,843,844],{"class":90,"line":551},[88,845,684],{},[88,847,848],{"class":90,"line":556},[88,849,850],{},"    async with httpx.AsyncClient() as client:\n",[88,852,853],{"class":90,"line":562},[88,854,855],{},"        resp = await client.get(\"https:\u002F\u002Fapi.example.com\u002Fping\")\n",[88,857,858],{"class":90,"line":567},[88,859,860],{},"    assert resp.status_code == 204\n",[10,862,863,864,866,867,870,871,873,874,40],{},"Because ",[14,865,24],{}," returns real ",[14,868,869],{},"httpx.Response"," objects, streaming, ",[14,872,357],{},", and content decoding behave identically to production. For async tests, mind fixture and loop scoping as described in ",[140,875,877],{"href":876},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002Fhow-to-scope-pytest-fixtures-for-async-tests\u002F","Scoping Pytest Fixtures for Async Tests",[364,879,881],{"id":880},"_5-the-fixture-driven-alternative-requests-mock","5. The fixture-driven alternative: requests-mock",[10,883,884,886,887,889,890,892],{},[14,885,286],{}," covers the same ground as ",[14,888,20],{}," for the ",[14,891,175],{}," library but ships a first-class pytest fixture, which suits suites that prefer dependency injection over decorators.",[79,894,896],{"className":381,"code":895,"language":383,"meta":84,"style":84},"def test_with_requests_mock(requests_mock):  # fixture from the requests-mock plugin\n    requests_mock.get(\n        \"https:\u002F\u002Fapi.example.com\u002Fusers\u002F7\",\n        json={\"id\": 7, \"name\": \"Ada\"},\n    )\n    import requests\n    resp = requests.get(\"https:\u002F\u002Fapi.example.com\u002Fusers\u002F7\")\n    assert resp.json()[\"id\"] == 7\n    assert requests_mock.call_count == 1\n",[14,897,898,903,908,912,917,921,926,931,936],{"__ignoreMap":84},[88,899,900],{"class":90,"line":91},[88,901,902],{},"def test_with_requests_mock(requests_mock):  # fixture from the requests-mock plugin\n",[88,904,905],{"class":90,"line":115},[88,906,907],{},"    requests_mock.get(\n",[88,909,910],{"class":90,"line":129},[88,911,669],{},[88,913,914],{"class":90,"line":455},[88,915,916],{},"        json={\"id\": 7, \"name\": \"Ada\"},\n",[88,918,919],{"class":90,"line":462},[88,920,684],{},[88,922,923],{"class":90,"line":468},[88,924,925],{},"    import requests\n",[88,927,928],{"class":90,"line":474},[88,929,930],{},"    resp = requests.get(\"https:\u002F\u002Fapi.example.com\u002Fusers\u002F7\")\n",[88,932,933],{"class":90,"line":480},[88,934,935],{},"    assert resp.json()[\"id\"] == 7\n",[88,937,938],{"class":90,"line":486},[88,939,940],{},"    assert requests_mock.call_count == 1\n",[10,942,943,944,21,946,948],{},"The choice between ",[14,945,20],{},[14,947,286],{}," is largely stylistic: decorator\u002Fcontext-manager versus fixture. Pick one per codebase to avoid two overlapping registries fighting over the same adapter.",[364,950,952],{"id":951},"_6-when-to-escalate-to-a-fake-server","6. When to escalate to a fake server",[10,954,955],{},"Stubs replace the transport, so they cannot test the transport. When your code owns connection pooling, redirects, chunked streaming, or TLS behaviour — or when you want to assert the exact bytes on the wire — run a real local server.",[79,957,959],{"className":381,"code":958,"language":383,"meta":84,"style":84},"def test_against_fake_server(httpserver):  # pytest-httpserver fixture\n    httpserver.expect_request(\"\u002Fusers\u002F7\").respond_with_json(\n        {\"id\": 7, \"name\": \"Ada\"}\n    )\n    import requests\n    # A REAL socket connects to 127.0.0.1 — keep loopback allowed in pytest-socket.\n    resp = requests.get(httpserver.url_for(\"\u002Fusers\u002F7\"))\n    assert resp.json()[\"name\"] == \"Ada\"\n",[14,960,961,966,971,976,980,984,989,994],{"__ignoreMap":84},[88,962,963],{"class":90,"line":91},[88,964,965],{},"def test_against_fake_server(httpserver):  # pytest-httpserver fixture\n",[88,967,968],{"class":90,"line":115},[88,969,970],{},"    httpserver.expect_request(\"\u002Fusers\u002F7\").respond_with_json(\n",[88,972,973],{"class":90,"line":129},[88,974,975],{},"        {\"id\": 7, \"name\": \"Ada\"}\n",[88,977,978],{"class":90,"line":455},[88,979,684],{},[88,981,982],{"class":90,"line":462},[88,983,925],{},[88,985,986],{"class":90,"line":468},[88,987,988],{},"    # A REAL socket connects to 127.0.0.1 — keep loopback allowed in pytest-socket.\n",[88,990,991],{"class":90,"line":474},[88,992,993],{},"    resp = requests.get(httpserver.url_for(\"\u002Fusers\u002F7\"))\n",[88,995,996],{"class":90,"line":480},[88,997,808],{},[10,999,1000,1001,1004],{},"This exercises the full networking stack against ",[14,1002,1003],{},"127.0.0.1",", so it catches transport bugs a stub never would, at the cost of being slower and requiring the loopback allowance from step 1.",[27,1006,1008],{"id":1007},"verification","Verification",[10,1010,1011],{},"Confirm the suite is genuinely isolated and your stubs are tight:",[32,1013,1014,1024,1034,1050,1061],{},[35,1015,1016,1017,1020,1021,1023],{},"Run ",[14,1018,1019],{},"pytest --disable-socket"," and watch for ",[14,1022,377],{},"; a clean pass proves no test reaches the network.",[35,1025,1026,1027,1029,1030,1033],{},"In ",[14,1028,20],{},", set ",[14,1031,1032],{},"assert_all_requests_are_fired=True"," (the default for the context-manager form) so a registered-but-unused stub fails the test rather than rotting silently.",[35,1035,1026,1036,1038,1039,21,1042,1045,1046,1049],{},[14,1037,24],{},", assert ",[14,1040,1041],{},"route.called",[14,1043,1044],{},"route.call_count"," on each route; use ",[14,1047,1048],{},"assert_all_called"," to catch dead routes.",[35,1051,1052,1053,1056,1057,1060],{},"Diff ",[14,1054,1055],{},"len(responses.calls)"," (or ",[14,1058,1059],{},"requests_mock.call_count",") against the expected number to detect accidental retries or duplicate requests.",[35,1062,1063,1064,1067,1068,1071],{},"Run the suite under ",[14,1065,1066],{},"pytest -p no:randomly","-off ordering or with ",[14,1069,1070],{},"pytest-randomly"," enabled to confirm no stub leaks across tests.",[27,1073,1075],{"id":1074},"troubleshooting","Troubleshooting",[1077,1078,1079,1095],"table",{},[1080,1081,1082],"thead",{},[1083,1084,1085,1089,1092],"tr",{},[1086,1087,1088],"th",{},"Symptom",[1086,1090,1091],{},"Root cause",[1086,1093,1094],{},"Fix",[1096,1097,1098,1117,1139,1165,1184,1203],"tbody",{},[1083,1099,1100,1104,1110],{},[1101,1102,1103],"td",{},"Test passes but production hits the network",[1101,1105,1106,1107,1109],{},"Patched ",[14,1108,159],{}," instead of the consuming module's lookup name",[1101,1111,1112,1113,1116],{},"Patch ",[14,1114,1115],{},"mypkg.module.requests.get",", the name where the call site resolves it",[1083,1118,1119,1125,1132],{},[1101,1120,1121,1124],{},[14,1122,1123],{},"ConnectionError"," \u002F stub never matches",[1101,1126,1127,1128,1131],{},"URL, method, or query string differs (trailing slash, ",[14,1129,1130],{},"?"," params)",[1101,1133,1134,1135,1138],{},"Match the exact URL or add a query-string matcher; print ",[14,1136,1137],{},"responses.calls"," to see the real request",[1083,1140,1141,1146,1155],{},[1101,1142,1143,1145],{},[14,1144,20],{}," ignores httpx traffic",[1101,1147,1148,1150,1151,614,1153],{},[14,1149,20],{}," only patches ",[14,1152,175],{},[14,1154,183],{},[1101,1156,1157,1158,1160,1161,1164],{},"Use ",[14,1159,24],{}," (or httpx ",[14,1162,1163],{},"MockTransport",") for httpx clients",[1083,1166,1167,1172,1177],{},[1101,1168,1169,1171],{},[14,1170,377],{}," on a legitimate local server",[1101,1173,1174,1176],{},[14,1175,373],{}," blocks loopback too",[1101,1178,1179,1180,1183],{},"Add ",[14,1181,1182],{},"--allow-hosts=127.0.0.1,::1"," for fake-server tests",[1083,1185,1186,1189,1192],{},[1101,1187,1188],{},"Stub fires for the wrong test",[1101,1190,1191],{},"Decorator\u002Ffixture scope leaked the registry",[1101,1193,1194,1195,1198,1199,1202],{},"Use per-test ",[14,1196,1197],{},"@responses.activate"," \u002F ",[14,1200,1201],{},"@respx.mock","; never register at module import time",[1083,1204,1205,1210,1213],{},[1101,1206,1207],{},[14,1208,1209],{},"AssertionError: not all requests fired",[1101,1211,1212],{},"A registered response was never requested",[1101,1214,1215],{},"Remove the dead stub or assert the code path that should consume it",[27,1217,1219],{"id":1218},"frequently-asked-questions","Frequently Asked Questions",[10,1221,1222,1225,1226,1228,1229,1231],{},[169,1223,1224],{},"Should I patch requests.get or use a library like responses?","\nPatch a single call site only for a one-off; use ",[14,1227,20],{}," or ",[14,1230,24],{}," as soon as a test exercises real client behaviour like retries, sessions, or query-string matching. Hand-rolled patches return naked dicts that skip status codes, headers, and JSON decoding, so they pass while production parsing breaks.",[10,1233,1234,1237,1238,1240,1241,1244,1245,1248,1249,1251],{},[169,1235,1236],{},"How do I stop a test suite from making real network calls by accident?","\nInstall ",[14,1239,373],{}," and run with ",[14,1242,1243],{},"--disable-socket",", then allow loopback with ",[14,1246,1247],{},"--allow-hosts=127.0.0.1"," for tests that use a local fake server. Any unmocked outbound call then raises ",[14,1250,377],{}," instead of silently hitting production.",[10,1253,1254,1257,1258,1260],{},[169,1255,1256],{},"When should I run a fake HTTP server instead of mocking the client?","\nUse a fake server such as ",[14,1259,268],{}," when the code under test owns the transport (custom connection pooling, TLS, redirects, streaming) or when you integration-test the wire format. Mock the client when you only care that your code reacts correctly to a given response body or status.",[10,1262,1263,1266,1268,1269,21,1271,1273,1274,1276,1277,1279,1280,1282,1283,1285],{},[169,1264,1265],{},"Do responses and respx work for async httpx clients?",[14,1267,24],{}," patches both ",[14,1270,740],{},[14,1272,744],{}," through httpx's transport layer, so it covers sync and async in one API. ",[14,1275,20],{}," targets ",[14,1278,175],{}," and does not intercept httpx; use ",[14,1281,24],{}," or httpx's own ",[14,1284,1163],{}," for httpx code.",[27,1287,1289],{"id":1288},"related-guides","Related guides",[32,1291,1292,1302,1309,1316,1329],{},[35,1293,1294,1295,1298,1299,1301],{},"The companion deep dive on ",[140,1296,1297],{"href":721},"mocking requests with the responses library"," covers registries, matchers, and ",[14,1300,717],{}," in full.",[35,1303,1304,1305,1308],{},"Getting the patch target right is the recurring failure here; ",[140,1306,1307],{"href":142},"patching strategies for complex codebases"," explains namespace resolution end to end.",[35,1310,1311,1312,1315],{},"When you build the response objects by hand, ",[140,1313,1314],{"href":147},"autospec and strict mocking"," keeps those doubles honest against the real client signatures.",[35,1317,1318,1319,62,1321,1324,1325,40],{},"For the mechanics of ",[14,1320,349],{},[14,1322,1323],{},"AsyncMock",", and awaitable responses behind async HTTP doubles, see the ",[140,1326,1328],{"href":1327},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002F","deep dive into unittest.mock",[35,1330,1331,1332,40],{},"When you want randomized payloads and URLs rather than fixed fixtures, drive the stubs with ",[140,1333,1335],{"href":1334},"\u002Fproperty-based-fuzz-testing-strategies\u002F","property-based and fuzz testing strategies",[10,1337,1338,1339],{},"← Back to ",[140,1340,1342],{"href":1341},"\u002Fadvanced-mocking-test-doubles-in-python\u002F","Advanced Mocking & Test Doubles in Python",[1344,1345,1346],"style",{},"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}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":84,"searchDepth":115,"depth":115,"links":1348},[1349,1350,1351,1359,1360,1361,1362],{"id":29,"depth":115,"text":30},{"id":152,"depth":115,"text":153},{"id":361,"depth":115,"text":362,"children":1352},[1353,1354,1355,1356,1357,1358],{"id":366,"depth":129,"text":367},{"id":410,"depth":129,"text":411},{"id":602,"depth":129,"text":603},{"id":725,"depth":129,"text":726},{"id":880,"depth":129,"text":881},{"id":951,"depth":129,"text":952},{"id":1007,"depth":115,"text":1008},{"id":1074,"depth":115,"text":1075},{"id":1218,"depth":115,"text":1219},{"id":1288,"depth":115,"text":1289},"Mock requests and httpx in pytest: monkeypatch vs the responses and respx libraries, requests-mock, blocking sockets, and when a fake server beats a mock.","md",{"slug":1366,"type":1367,"breadcrumb":1368,"datePublished":1369,"dateModified":1369,"faq":1370,"howto":1379},"mocking-network-and-http-calls","cluster","Mocking HTTP Calls","2026-06-18",[1371,1373,1375,1377],{"q":1224,"a":1372},"Patch a single call site only for a one-off; use responses or respx as soon as a test exercises real client behaviour like retries, sessions, or query-string matching. Hand-rolled patches return naked dicts that skip status codes, headers, and JSON decoding, so they pass while production parsing breaks.",{"q":1236,"a":1374},"Install pytest-socket and run with --disable-socket, then allow loopback with --allow-hosts=127.0.0.1 for tests that use a local fake server. Any unmocked outbound call then raises SocketBlockedError instead of silently hitting production.",{"q":1256,"a":1376},"Use a fake server such as pytest-httpserver when the code under test owns the transport (custom connection pooling, TLS, redirects, streaming) or when you integration-test the wire format. Mock the client when you only care that your code reacts correctly to a given response body or status.",{"q":1265,"a":1378},"respx patches both httpx.Client and httpx.AsyncClient through httpx's transport layer, so it covers sync and async in one API. responses targets requests and does not intercept httpx; use respx or httpx's own MockTransport for httpx code.",{"name":1380,"description":1381,"steps":1382},"How to mock network and HTTP calls in pytest","Pick an interception layer, register expected responses, block real sockets, and assert the requests your code actually made.",[1383,1386,1389,1392,1395],{"name":1384,"text":1385},"Block real sockets first","Add pytest-socket and run with --disable-socket so any unmocked outbound call fails loudly instead of reaching a live service.",{"name":1387,"text":1388},"Pick the interception layer","Use responses or requests-mock for the requests library, respx for httpx, and a fake server like pytest-httpserver when the transport itself is under test.",{"name":1390,"text":1391},"Register expected responses","Declare each URL, method, status, and body up front with matchers for query strings, headers, or JSON so the stub only fires on the intended request.",{"name":1393,"text":1394},"Exercise the client through its real public API","Call the code under test with a real Session or httpx.Client so retries, headers, and JSON decoding run exactly as in production.",{"name":1396,"text":1397},"Assert the requests that were made","Verify call count, order, and request bodies, and assert that every registered response was actually fired so dead stubs do not hide regressions.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls",{"title":5,"description":1363},"advanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Findex","w_GYx9piintfB8Xr59cXv1zhgEDCECKu4f36HEzXY4M",1781793487880]