[{"data":1,"prerenderedAt":761},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-requests-with-the-responses-library\u002F":3},{"id":4,"title":5,"body":6,"description":724,"extension":725,"meta":726,"navigation":140,"path":757,"seo":758,"stem":759,"__hash__":760},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-requests-with-the-responses-library\u002Findex.md","Mocking requests with the responses Library",{"type":7,"value":8,"toc":717},"minimark",[9,41,46,102,106,415,418,481,484,537,541,567,571,656,660,677,697,708,713],[10,11,12,13,17,18,21,22,24,25,28,29,32,33,36,37,40],"p",{},"You need to test code that calls a third-party REST API through the ",[14,15,16],"code",{},"requests"," library — including its retry logic, custom headers, and error handling — without the test ever touching the network. The ",[14,19,20],{},"responses"," library patches ",[14,23,16],{}," at the transport-adapter layer, so your real ",[14,26,27],{},"Session"," runs while canned responses are replayed and every call is recorded for assertion. This guide covers the registration API (",[14,30,31],{},"@responses.activate",", ",[14,34,35],{},"responses.add","), ordered and unordered registries, matchers for query strings and bodies, and the ",[14,38,39],{},"assert_all_requests_are_fired"," guard that turns a forgotten stub into a failing test instead of silent dead code.",[42,43,45],"h2",{"id":44},"prerequisites","Prerequisites",[47,48,49,72,81],"ul",{},[50,51,52,55,56,59,60,63,64,67,68,71],"li",{},[14,53,54],{},"responses >= 0.25"," (the ",[14,57,58],{},"registries"," and ",[14,61,62],{},"responses.matchers"," modules referenced here are stable from 0.21+; ",[14,65,66],{},"OrderedRegistry"," lives in ",[14,69,70],{},"responses.registries",").",[50,73,74,59,77,80],{},[14,75,76],{},"requests >= 2.31",[14,78,79],{},"pytest >= 8.0",".",[50,82,83,85,86,88,89,92,93,96,97,80],{},[14,84,20],{}," intercepts only ",[14,87,16],{},"; for ",[14,90,91],{},"httpx"," use ",[14,94,95],{},"respx"," instead, as covered in ",[98,99,101],"a",{"href":100},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002F","Mocking Network and HTTP Calls",[42,103,105],{"id":104},"solution","Solution",[107,108,113],"pre",{"className":109,"code":110,"language":111,"meta":112,"style":112},"language-python shiki shiki-themes github-light github-dark","import requests\nimport responses\nfrom responses import matchers\n\n# Code under test: a thin client with one retry on 429.\ndef fetch_page(session: requests.Session, page: int) -> dict:\n    for _ in range(2):                                   # one retry\n        resp = session.get(\n            \"https:\u002F\u002Fapi.example.com\u002Fitems\",\n            params={\"page\": page},\n            headers={\"Accept\": \"application\u002Fjson\"},\n        )\n        if resp.status_code == 429:\n            continue                                     # retry once on rate-limit\n        resp.raise_for_status()\n        return resp.json()\n    resp.raise_for_status()\n    return resp.json()\n\n\n@responses.activate                                      # patches requests for this test only\ndef test_fetch_page_retries_then_succeeds():\n    # First registration -> 429; second registration -> 200. Same method+URL,\n    # consumed in order, so call 1 gets the 429 and call 2 gets the 200.\n    responses.add(\n        responses.GET,\n        \"https:\u002F\u002Fapi.example.com\u002Fitems\",\n        json={\"error\": \"rate_limit\"},\n        status=429,\n        match=[matchers.query_param_matcher({\"page\": \"2\"})],  # only fire for page=2\n    )\n    responses.add(\n        responses.GET,\n        \"https:\u002F\u002Fapi.example.com\u002Fitems\",\n        json={\"items\": [1, 2, 3]},\n        status=200,\n        match=[matchers.query_param_matcher({\"page\": \"2\"})],\n    )\n\n    with requests.Session() as session:                  # the REAL client\n        result = fetch_page(session, page=2)\n\n    assert result == {\"items\": [1, 2, 3]}\n\n    # responses records every intercepted call in order.\n    assert len(responses.calls) == 2\n    assert responses.calls[0].response.status_code == 429\n    assert responses.calls[1].response.status_code == 200\n    # The query string and header reached the transport unchanged.\n    assert responses.calls[1].request.params == {\"page\": \"2\"}\n    assert responses.calls[1].request.headers[\"Accept\"] == \"application\u002Fjson\"\n","python","",[14,114,115,123,129,135,142,148,154,160,166,172,178,184,190,196,202,208,214,220,226,231,236,242,248,254,260,266,272,278,284,290,296,302,307,312,317,323,329,335,340,345,351,357,362,368,373,379,385,391,397,403,409],{"__ignoreMap":112},[116,117,120],"span",{"class":118,"line":119},"line",1,[116,121,122],{},"import requests\n",[116,124,126],{"class":118,"line":125},2,[116,127,128],{},"import responses\n",[116,130,132],{"class":118,"line":131},3,[116,133,134],{},"from responses import matchers\n",[116,136,138],{"class":118,"line":137},4,[116,139,141],{"emptyLinePlaceholder":140},true,"\n",[116,143,145],{"class":118,"line":144},5,[116,146,147],{},"# Code under test: a thin client with one retry on 429.\n",[116,149,151],{"class":118,"line":150},6,[116,152,153],{},"def fetch_page(session: requests.Session, page: int) -> dict:\n",[116,155,157],{"class":118,"line":156},7,[116,158,159],{},"    for _ in range(2):                                   # one retry\n",[116,161,163],{"class":118,"line":162},8,[116,164,165],{},"        resp = session.get(\n",[116,167,169],{"class":118,"line":168},9,[116,170,171],{},"            \"https:\u002F\u002Fapi.example.com\u002Fitems\",\n",[116,173,175],{"class":118,"line":174},10,[116,176,177],{},"            params={\"page\": page},\n",[116,179,181],{"class":118,"line":180},11,[116,182,183],{},"            headers={\"Accept\": \"application\u002Fjson\"},\n",[116,185,187],{"class":118,"line":186},12,[116,188,189],{},"        )\n",[116,191,193],{"class":118,"line":192},13,[116,194,195],{},"        if resp.status_code == 429:\n",[116,197,199],{"class":118,"line":198},14,[116,200,201],{},"            continue                                     # retry once on rate-limit\n",[116,203,205],{"class":118,"line":204},15,[116,206,207],{},"        resp.raise_for_status()\n",[116,209,211],{"class":118,"line":210},16,[116,212,213],{},"        return resp.json()\n",[116,215,217],{"class":118,"line":216},17,[116,218,219],{},"    resp.raise_for_status()\n",[116,221,223],{"class":118,"line":222},18,[116,224,225],{},"    return resp.json()\n",[116,227,229],{"class":118,"line":228},19,[116,230,141],{"emptyLinePlaceholder":140},[116,232,234],{"class":118,"line":233},20,[116,235,141],{"emptyLinePlaceholder":140},[116,237,239],{"class":118,"line":238},21,[116,240,241],{},"@responses.activate                                      # patches requests for this test only\n",[116,243,245],{"class":118,"line":244},22,[116,246,247],{},"def test_fetch_page_retries_then_succeeds():\n",[116,249,251],{"class":118,"line":250},23,[116,252,253],{},"    # First registration -> 429; second registration -> 200. Same method+URL,\n",[116,255,257],{"class":118,"line":256},24,[116,258,259],{},"    # consumed in order, so call 1 gets the 429 and call 2 gets the 200.\n",[116,261,263],{"class":118,"line":262},25,[116,264,265],{},"    responses.add(\n",[116,267,269],{"class":118,"line":268},26,[116,270,271],{},"        responses.GET,\n",[116,273,275],{"class":118,"line":274},27,[116,276,277],{},"        \"https:\u002F\u002Fapi.example.com\u002Fitems\",\n",[116,279,281],{"class":118,"line":280},28,[116,282,283],{},"        json={\"error\": \"rate_limit\"},\n",[116,285,287],{"class":118,"line":286},29,[116,288,289],{},"        status=429,\n",[116,291,293],{"class":118,"line":292},30,[116,294,295],{},"        match=[matchers.query_param_matcher({\"page\": \"2\"})],  # only fire for page=2\n",[116,297,299],{"class":118,"line":298},31,[116,300,301],{},"    )\n",[116,303,305],{"class":118,"line":304},32,[116,306,265],{},[116,308,310],{"class":118,"line":309},33,[116,311,271],{},[116,313,315],{"class":118,"line":314},34,[116,316,277],{},[116,318,320],{"class":118,"line":319},35,[116,321,322],{},"        json={\"items\": [1, 2, 3]},\n",[116,324,326],{"class":118,"line":325},36,[116,327,328],{},"        status=200,\n",[116,330,332],{"class":118,"line":331},37,[116,333,334],{},"        match=[matchers.query_param_matcher({\"page\": \"2\"})],\n",[116,336,338],{"class":118,"line":337},38,[116,339,301],{},[116,341,343],{"class":118,"line":342},39,[116,344,141],{"emptyLinePlaceholder":140},[116,346,348],{"class":118,"line":347},40,[116,349,350],{},"    with requests.Session() as session:                  # the REAL client\n",[116,352,354],{"class":118,"line":353},41,[116,355,356],{},"        result = fetch_page(session, page=2)\n",[116,358,360],{"class":118,"line":359},42,[116,361,141],{"emptyLinePlaceholder":140},[116,363,365],{"class":118,"line":364},43,[116,366,367],{},"    assert result == {\"items\": [1, 2, 3]}\n",[116,369,371],{"class":118,"line":370},44,[116,372,141],{"emptyLinePlaceholder":140},[116,374,376],{"class":118,"line":375},45,[116,377,378],{},"    # responses records every intercepted call in order.\n",[116,380,382],{"class":118,"line":381},46,[116,383,384],{},"    assert len(responses.calls) == 2\n",[116,386,388],{"class":118,"line":387},47,[116,389,390],{},"    assert responses.calls[0].response.status_code == 429\n",[116,392,394],{"class":118,"line":393},48,[116,395,396],{},"    assert responses.calls[1].response.status_code == 200\n",[116,398,400],{"class":118,"line":399},49,[116,401,402],{},"    # The query string and header reached the transport unchanged.\n",[116,404,406],{"class":118,"line":405},50,[116,407,408],{},"    assert responses.calls[1].request.params == {\"page\": \"2\"}\n",[116,410,412],{"class":118,"line":411},51,[116,413,414],{},"    assert responses.calls[1].request.headers[\"Accept\"] == \"application\u002Fjson\"\n",[10,416,417],{},"For finer control over consumption order, use an explicit registry:",[107,419,421],{"className":109,"code":420,"language":111,"meta":112,"style":112},"import responses\nfrom responses import registries\n\n# OrderedRegistry forces responses to be consumed strictly in registration\n# order and errors if a request arrives out of sequence.\n@responses.activate(registry=registries.OrderedRegistry)\ndef test_strict_ordering():\n    responses.add(responses.GET, \"https:\u002F\u002Fapi.example.com\u002Fa\", json={\"step\": 1})\n    responses.add(responses.GET, \"https:\u002F\u002Fapi.example.com\u002Fb\", json={\"step\": 2})\n    import requests\n    assert requests.get(\"https:\u002F\u002Fapi.example.com\u002Fa\").json()[\"step\"] == 1\n    assert requests.get(\"https:\u002F\u002Fapi.example.com\u002Fb\").json()[\"step\"] == 2\n",[14,422,423,427,432,436,441,446,451,456,461,466,471,476],{"__ignoreMap":112},[116,424,425],{"class":118,"line":119},[116,426,128],{},[116,428,429],{"class":118,"line":125},[116,430,431],{},"from responses import registries\n",[116,433,434],{"class":118,"line":131},[116,435,141],{"emptyLinePlaceholder":140},[116,437,438],{"class":118,"line":137},[116,439,440],{},"# OrderedRegistry forces responses to be consumed strictly in registration\n",[116,442,443],{"class":118,"line":144},[116,444,445],{},"# order and errors if a request arrives out of sequence.\n",[116,447,448],{"class":118,"line":150},[116,449,450],{},"@responses.activate(registry=registries.OrderedRegistry)\n",[116,452,453],{"class":118,"line":156},[116,454,455],{},"def test_strict_ordering():\n",[116,457,458],{"class":118,"line":162},[116,459,460],{},"    responses.add(responses.GET, \"https:\u002F\u002Fapi.example.com\u002Fa\", json={\"step\": 1})\n",[116,462,463],{"class":118,"line":168},[116,464,465],{},"    responses.add(responses.GET, \"https:\u002F\u002Fapi.example.com\u002Fb\", json={\"step\": 2})\n",[116,467,468],{"class":118,"line":174},[116,469,470],{},"    import requests\n",[116,472,473],{"class":118,"line":180},[116,474,475],{},"    assert requests.get(\"https:\u002F\u002Fapi.example.com\u002Fa\").json()[\"step\"] == 1\n",[116,477,478],{"class":118,"line":186},[116,479,480],{},"    assert requests.get(\"https:\u002F\u002Fapi.example.com\u002Fb\").json()[\"step\"] == 2\n",[10,482,483],{},"The context-manager form makes the firing guard explicit:",[107,485,487],{"className":109,"code":486,"language":111,"meta":112,"style":112},"import responses\n\ndef test_context_manager_guard():\n    # assert_all_requests_are_fired defaults to True here: an unused\n    # registration fails the test on context exit.\n    with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps:\n        rsps.add(responses.GET, \"https:\u002F\u002Fapi.example.com\u002Fused\", json={\"ok\": True})\n        import requests\n        requests.get(\"https:\u002F\u002Fapi.example.com\u002Fused\")\n        # If you added an unused stub, the block would raise AssertionError on exit.\n",[14,488,489,493,497,502,507,512,517,522,527,532],{"__ignoreMap":112},[116,490,491],{"class":118,"line":119},[116,492,128],{},[116,494,495],{"class":118,"line":125},[116,496,141],{"emptyLinePlaceholder":140},[116,498,499],{"class":118,"line":131},[116,500,501],{},"def test_context_manager_guard():\n",[116,503,504],{"class":118,"line":137},[116,505,506],{},"    # assert_all_requests_are_fired defaults to True here: an unused\n",[116,508,509],{"class":118,"line":144},[116,510,511],{},"    # registration fails the test on context exit.\n",[116,513,514],{"class":118,"line":150},[116,515,516],{},"    with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps:\n",[116,518,519],{"class":118,"line":156},[116,520,521],{},"        rsps.add(responses.GET, \"https:\u002F\u002Fapi.example.com\u002Fused\", json={\"ok\": True})\n",[116,523,524],{"class":118,"line":162},[116,525,526],{},"        import requests\n",[116,528,529],{"class":118,"line":168},[116,530,531],{},"        requests.get(\"https:\u002F\u002Fapi.example.com\u002Fused\")\n",[116,533,534],{"class":118,"line":174},[116,535,536],{},"        # If you added an unused stub, the block would raise AssertionError on exit.\n",[42,538,540],{"id":539},"why-this-works","Why this works",[10,542,543,545,546,549,550,552,553,556,557,559,560,563,564,566],{},[14,544,20],{}," registers a custom ",[14,547,548],{},"HTTPAdapter"," on the ",[14,551,16],{}," Session machinery, so requests never reach ",[14,554,555],{},"urllib3","'s connection pool or a socket. Because interception happens below your client code, the real ",[14,558,27],{},", parameter encoding, header injection, and ",[14,561,562],{},"Response.json()"," decoding all execute exactly as in production — the only thing replaced is the bytes coming back off the wire. Registrations for the same method and URL form an ordered queue that is consumed one per matching request, which is what makes retry and pagination sequences testable, and ",[14,565,39],{}," inverts the check so a stub you forgot to exercise becomes a loud failure rather than silent coverage rot.",[42,568,570],{"id":569},"edge-cases-and-failure-modes","Edge cases and failure modes",[47,572,573,595,605,619,634],{},[50,574,575,579,580,59,583,586,587,590,591,594],{},[576,577,578],"strong",{},"Trailing slash and query mismatches."," ",[14,581,582],{},"https:\u002F\u002Fapi.example.com\u002Fitems",[14,584,585],{},"...\u002Fitems\u002F"," are different URLs, and a query string the code adds but the stub omits will not match. Use ",[14,588,589],{},"matchers.query_param_matcher"," (or ",[14,592,593],{},"query_string_matcher",") rather than baking params into the URL.",[50,596,597,600,601,604],{},[576,598,599],{},"Unconsumed registrations."," With ",[14,602,603],{},"assert_all_requests_are_fired=True",", a leftover stub fails the test. Keep it on to catch dead branches; turn it off only for deliberately optional fallbacks.",[50,606,607,610,611,615,616,618],{},[576,608,609],{},"Queue exhaustion."," Once all registrations for a URL are consumed, the ",[612,613,614],"em",{},"last"," one repeats for further calls. If you expect an error after N calls, register exactly N responses with an ",[14,617,66],{}," so an extra call raises instead of silently replaying.",[50,620,621,579,624,627,628,631,632,80],{},[576,622,623],{},"Passthrough leakage.",[14,625,626],{},"responses.add_passthru(prefix)"," lets specific hosts reach the real network — combine it with ",[14,629,630],{},"pytest-socket"," allow-hosts so the rest of the suite stays blocked, as described in ",[98,633,101],{"href":100},[50,635,636,579,639,641,642,32,644,647,648,651,652,80],{},[576,637,638],{},"Wrong patch surface for non-requests clients.",[14,640,20],{}," does nothing for ",[14,643,91],{},[14,645,646],{},"aiohttp",", or raw ",[14,649,650],{},"urllib",". Getting the interception layer right is the same discipline as choosing a ",[98,653,655],{"href":654},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002F","patch target",[42,657,659],{"id":658},"frequently-asked-questions","Frequently Asked Questions",[10,661,662,665,666,669,670,672,673,676],{},[576,663,664],{},"What does assert_all_requests_are_fired do in responses?","\nWhen ",[14,667,668],{},"True"," it fails the test if any registered response was never matched by a request, catching dead stubs and skipped code paths. It defaults to ",[14,671,668],{}," for the ",[14,674,675],{},"RequestsMock"," context manager and can be toggled per block.",[10,678,679,682,683,685,686,688,689,692,693,696],{},[576,680,681],{},"How do I match a request by query string or JSON body with responses?","\nPass matchers from ",[14,684,62],{}," to ",[14,687,35],{},", for example ",[14,690,691],{},"matchers.query_param_matcher({'page': '2'})"," or ",[14,694,695],{},"matchers.json_params_matcher({'id': 7})",". A registration only fires when every supplied matcher passes against the incoming request.",[10,698,699,702,703,705,706,80],{},[576,700,701],{},"Can responses return a different response on each call to the same URL?","\nYes. Register the same method and URL multiple times; ",[14,704,20],{}," consumes registrations in order, so the first call gets the first registration and so on. The last registration repeats once the queue is exhausted unless you use an ",[14,707,66],{},[10,709,710,711],{},"← Back to ",[98,712,101],{"href":100},[714,715,716],"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":112,"searchDepth":125,"depth":125,"links":718},[719,720,721,722,723],{"id":44,"depth":125,"text":45},{"id":104,"depth":125,"text":105},{"id":539,"depth":125,"text":540},{"id":569,"depth":125,"text":570},{"id":658,"depth":125,"text":659},"Use the responses library to mock requests in pytest: @responses.activate, responses.add, registries, matchers, and assert_all_requests_are_fired for tight stubs.","md",{"slug":727,"type":728,"breadcrumb":729,"datePublished":730,"dateModified":730,"faq":731,"howto":738},"mocking-requests-with-the-responses-library","long_tail","responses Library","2026-06-18",[732,734,736],{"q":664,"a":733},"When True it fails the test if any registered response was never matched by a request, catching dead stubs and skipped code paths. It defaults to True for the RequestsMock context manager and can be toggled per block.",{"q":681,"a":735},"Pass matchers from responses.matchers to responses.add, for example matchers.query_param_matcher({'page': '2'}) or matchers.json_params_matcher({'id': 7}). A registration only fires when every supplied matcher passes against the incoming request.",{"q":701,"a":737},"Yes. Register the same method and URL multiple times; responses consumes registrations in order, so the first call gets the first registration and so on. The last registration repeats once the queue is exhausted unless you use an OrderedRegistry.",{"name":739,"description":740,"steps":741},"How to mock requests with the responses library","Activate responses, register expected responses with matchers, drive the real client, and assert every stub fired.",[742,745,748,751,754],{"name":743,"text":744},"Activate responses","Wrap the test in @responses.activate or use the responses.RequestsMock context manager so the requests transport adapter is patched for the test's lifetime.",{"name":746,"text":747},"Register expected responses","Call responses.add with the method, URL, json or body, and status for each request the code should make.",{"name":749,"text":750},"Tighten matching","Attach matchers from responses.matchers for query strings, headers, or JSON bodies so a stub only fires on the intended request shape.",{"name":752,"text":753},"Drive the real client","Call the code under test with a real requests Session so retries, headers, and JSON decoding run as in production.",{"name":755,"text":756},"Assert calls and firing","Inspect responses.calls for count and request bodies and keep assert_all_requests_are_fired on so unused stubs fail the test.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-requests-with-the-responses-library",{"title":5,"description":724},"advanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Fmocking-requests-with-the-responses-library\u002Findex","iZACsx3pRkfUJnOdG2vTPqLoZq8aTg-EYoPIjE9t4C0",1781793487970]