Package: pytest-httpx
Send responses to HTTPX using pytestVersion 1.0.0 will be released once httpx is considered as stable (release of 1.0.0).However, current state can be considered as stable.Once installed,httpx_mockpytestfixture will make sure everyhttpxrequest will be replied to with user provided responses.Add responsesJSON bodyCustom bodyMultipart body (files, ...)HTTP status codeHTTP headersHTTP/2.0Add dynamic responsesRaising exceptionsCheck requestsDo not mock some requestsMigratingresponsesaioresponsesAdd responsesYou can register responses for both sync and asyncHTTPXrequests.importpytestimporthttpxdeftest_something(httpx_mock):httpx_mock.add_response()withhttpx.Client()asclient:response=client.get("https://test_url")@pytest.mark.asyncioasyncdeftest_something_async(httpx_mock):httpx_mock.add_response()asyncwithhttpx.AsyncClient()asclient:response=awaitclient.get("https://test_url")If all registered responses are not sent back during test execution, the test case will fail at teardown.This behavior can be disabled thanks to theassert_all_responses_were_requestedfixture:importpytest@pytest.fixturedefassert_all_responses_were_requested()->bool:returnFalseDefault response is a HTTP/1.1 200 (OK) without any body.How response is selectedIn case more than one response match request, the first one not yet sent (according to the registration order) will be sent.In case all matching responses have been sent, the last one (according to the registration order) will be sent.You can add criteria so that response will be sent only in case of a more specific matching.Matching on URLurlparameter can either be a string, a pythonre.Patterninstance or ahttpx.URLinstance.Matching is performed on the full URL, query parameters included.Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once.importhttpxfrompytest_httpximportHTTPXMockdeftest_url(httpx_mock:HTTPXMock):httpx_mock.add_response(url="https://test_url?a=1&b=2")withhttpx.Client()asclient:response1=client.delete("https://test_url?a=1&b=2")response2=client.get("https://test_url?b=2&a=1")Matching on HTTP methodUsemethodparameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) to reply to.methodparameter must be a string. It will be upper-cased, so it can be provided lower cased.Matching is performed on equality.importhttpxfrompytest_httpximportHTTPXMockdeftest_post(httpx_mock:HTTPXMock):httpx_mock.add_response(method="POST")withhttpx.Client()asclient:response=client.post("https://test_url")deftest_put(httpx_mock:HTTPXMock):httpx_mock.add_response(method="PUT")withhttpx.Client()asclient:response=client.put("https://test_url")deftest_delete(httpx_mock:HTTPXMock):httpx_mock.add_response(method="DELETE")withhttpx.Client()asclient:response=client.delete("https://test_url")deftest_patch(httpx_mock:HTTPXMock):httpx_mock.add_response(method="PATCH")withhttpx.Client()asclient:response=client.patch("https://test_url")deftest_head(httpx_mock:HTTPXMock):httpx_mock.add_response(method="HEAD")withhttpx.Client()asclient:response=client.head("https://test_url")Matching on proxy URLproxy_urlparameter can either be a string, a pythonre.Patterninstance or ahttpx.URLinstance.Matching is performed on the full proxy URL, query parameters included.Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once.importhttpxfrompytest_httpximportHTTPXMockdeftest_proxy_url(httpx_mock:HTTPXMock):httpx_mock.add_response(proxy_url="http://test_proxy_url?b=1&a=2")withhttpx.Client(proxy="http://test_proxy_url?a=2&b=1")asclient:response=client.get("https://test_url")Matching on HTTP headersUsematch_headersparameter to specify the HTTP headers to reply to.Matching is performed on equality for each provided header.importhttpxfrompytest_httpximportHTTPXMockdeftest_headers_matching(httpx_mock:HTTPXMock):httpx_mock.add_response(match_headers={'User-Agent':'python-httpx/0.25.0'})withhttpx.Client()asclient:response=client.get("https://test_url")Matching on HTTP bodyUsematch_contentparameter to specify the full HTTP body to reply to.Matching is performed on equality.importhttpxfrompytest_httpximportHTTPXMockdeftest_content_matching(httpx_mock:HTTPXMock):httpx_mock.add_response(match_content=b"This is the body")withhttpx.Client()asclient:response=client.post("https://test_url",content=b"This is the body")Matching on HTTP JSON bodyUsematch_jsonparameter to specify the JSON decoded HTTP body to reply to.Matching is performed on equality. You can however useunittest.mock.ANYto do partial matching.importhttpxfrompytest_httpximportHTTPXMockfromunittest.mockimportANYdeftest_json_matching(httpx_mock:HTTPXMock):httpx_mock.add_response(match_json={"a":"json","b":2})withhttpx.Client()asclient:response=client.post("https://test_url",json={"a":"json","b":2})deftest_partial_json_matching(httpx_mock:HTTPXMock):httpx_mock.add_response(match_json={"a":"json","b":ANY})withhttpx.Client()asclient:response=client.post("https://test_url",json={"a":"json","b":2})Note thatmatch_contentcannot be provided ifmatch_jsonis also provided.Add JSON responseUsejsonparameter to add a JSON response using python values.importhttpxfrompytest_httpximportHTTPXMockdeftest_json(httpx_mock:HTTPXMock):httpx_mock.add_response(json=[{"key1":"value1","key2":"value2"}])withhttpx.Client()asclient:assertclient.get("https://test_url").json()==[{"key1":"value1","key2":"value2"}]Note that thecontent-typeheader will be set toapplication/jsonby default in the response.Reply with custom bodyUsetextparameter to reply with a custom body by providing UTF-8 encoded string.importhttpxfrompytest_httpximportHTTPXMockdeftest_str_body(httpx_mock:HTTPXMock):httpx_mock.add_response(text="This is my UTF-8 content")withhttpx.Client()asclient:assertclient.get("https://test_url").text=="This is my UTF-8 content"Usecontentparameter to reply with a custom body by providing bytes.importhttpxfrompytest_httpximportHTTPXMockdeftest_bytes_body(httpx_mock:HTTPXMock):httpx_mock.add_response(content=b"This is my bytes content")withhttpx.Client()asclient:assertclient.get("https://test_url").content==b"This is my bytes content"Usehtmlparameter to reply with a custom body by providing UTF-8 encoded string.importhttpxfrompytest_httpximportHTTPXMockdeftest_html_body(httpx_mock:HTTPXMock):httpx_mock.add_response(html="<body>This is <p> HTML content</body>")withhttpx.Client()asclient:assertclient.get("https://test_url").text=="<body>This is <p> HTML content</body>"Reply by streaming chunksUsestreamparameter to stream chunks that you specify.importhttpximportpytestfrompytest_httpximportHTTPXMock,IteratorStreamdeftest_sync_streaming(httpx_mock:HTTPXMock):httpx_mock.add_response(stream=IteratorStream([b"part 1",b"part 2"]))withhttpx.Client()asclient:withclient.stream(method="GET",url="https://test_url")asresponse:assertlist(response.iter_raw())==[b"part 1",b"part 2"]@pytest.mark.asyncioasyncdeftest_async_streaming(httpx_mock:HTTPXMock):httpx_mock.add_response(stream=IteratorStream([b"part 1",b"part 2"]))asyncwithhttpx.AsyncClient()asclient:asyncwithclient.stream(method="GET",url="https://test_url")asresponse:assert[partasyncforpartinresponse.aiter_raw()]==[b"part 1",b"part 2"]Add multipart responseUse the httpxMultipartStreamvia thestreamparameter to send a multipart response.Reach out tohttpxdevelopers if you need this publicly exposed asthis is not a standard use case.importhttpxfromhttpx._multipartimportMultipartStreamfrompytest_httpximportHTTPXMockdeftest_multipart_body(httpx_mock:HTTPXMock):httpx_mock.add_response(stream=MultipartStream(data={"key1":"value1"},files={"file1":b"content of file 1"},boundary=b"2256d3a36d2a61a1eba35a22bee5c74a"))withhttpx.Client()asclient:assertclient.get("https://test_url").text=='''--2256d3a36d2a61a1eba35a22bee5c74a\rContent-Disposition: form-data; name="key1"\r\rvalue1\r--2256d3a36d2a61a1eba35a22bee5c74a\rContent-Disposition: form-data; name="file1"; filename="upload"\rContent-Type: application/octet-stream\r\rcontent of file 1\r--2256d3a36d2a61a1eba35a22bee5c74a--\r'''Add non 200 responseUsestatus_codeparameter to specify the HTTP status code of the response.importhttpxfrompytest_httpximportHTTPXMockdeftest_status_code(httpx_mock:HTTPXMock):httpx_mock.add_response(status_code=404)withhttpx.Client()asclient:assertclient.get("https://test_url").status_code==404Reply with custom headersUseheadersparameter to specify the extra headers of the response.Any valid httpx headers type is supported, you can submit headers as a dict (str or bytes), a list of 2-tuples (str or bytes) or ahttpx.Headerinstance.importhttpxfrompytest_httpximportHTTPXMockdeftest_headers_as_str_dict(httpx_mock:HTTPXMock):httpx_mock.add_response(headers={"X-Header1":"Test value"})withhttpx.Client()asclient:assertclient.get("https://test_url").headers["x-header1"]=="Test value"deftest_headers_as_str_tuple_list(httpx_mock:HTTPXMock):httpx_mock.add_response(headers=[("X-Header1","Test value")])withhttpx.Client()asclient:assertclient.get("https://test_url").headers["x-header1"]=="Test value"deftest_headers_as_httpx_headers(httpx_mock:HTTPXMock):httpx_mock.add_response(headers=httpx.Headers({b"X-Header1":b"Test value"}))withhttpx.Client()asclient:assertclient.get("https://test_url").headers["x-header1"]=="Test value"Reply with cookiesCookies are sent in theset-cookieHTTP header.You can then send cookies in the response by setting theset-cookieheader withthe value following key=value format.importhttpxfrompytest_httpximportHTTPXMockdeftest_cookie(httpx_mock:HTTPXMock):httpx_mock.add_response(headers={"set-cookie":"key=value"})withhttpx.Client()asclient:response=client.get("https://test_url")assertdict(response.cookies)=={"key":"value"}deftest_cookies(httpx_mock:HTTPXMock):httpx_mock.add_response(headers=[("set-cookie","key=value"),("set-cookie","key2=value2")])withhttpx.Client()asclient:response=client.get("https://test_url")assertdict(response.cookies)=={"key":"value","key2":"value2"}Add HTTP/2.0 responseUsehttp_versionparameter to specify the HTTP protocol version of the response.importhttpxfrompytest_httpximportHTTPXMockdeftest_http_version(httpx_mock:HTTPXMock):httpx_mock.add_response(http_version="HTTP/2.0")withhttpx.Client()asclient:assertclient.get("https://test_url").http_version=="HTTP/2.0"Add callbacksYou can perform custom manipulation upon request reception by registering callbacks.Callback should expect one parameter, the receivedhttpx.Request.If all callbacks are not executed during test execution, the test case will fail at teardown.This behavior can be disabled thanks to theassert_all_responses_were_requestedfixture:importpytest@pytest.fixturedefassert_all_responses_were_requested()->bool:returnFalseNote that callbacks are considered as responses, and thus areselected the same way.Dynamic responsesCallback should return ahttpx.Response.importhttpxfrompytest_httpximportHTTPXMockdeftest_dynamic_response(httpx_mock:HTTPXMock):defcustom_response(request:httpx.Request):returnhttpx.Response(status_code=200,json={"url":str(request.url)},)httpx_mock.add_callback(custom_response)withhttpx.Client()asclient:response=client.get("https://test_url")assertresponse.json()=={"url":"https://test_url"}Alternatively, callbacks can also be asynchronous.As in the following sample simulating network latency on some responses only.importasyncioimporthttpximportpytestfrompytest_httpximportHTTPXMock@pytest.mark.asyncioasyncdeftest_dynamic_async_response(httpx_mock:HTTPXMock):asyncdefsimulate_network_latency(request:httpx.Request):awaitasyncio.sleep(1)returnhttpx.Response(status_code=200,json={"url":str(request.url)},)httpx_mock.add_callback(simulate_network_latency)httpx_mock.add_response()asyncwithhttpx.AsyncClient()asclient:responses=awaitasyncio.gather(# Response will be received after one secondclient.get("https://test_url"),# Response will instantly be received (1 second before the first request)client.get("https://test_url"))Raising exceptionsYou can simulate HTTPX exception throwing by raising an exception in your callback or usehttpx_mock.add_exceptionwith the exception instance.This can be useful if you want to assert that your code handles HTTPX exceptions properly.importhttpximportpytestfrompytest_httpximportHTTPXMockdeftest_exception_raising(httpx_mock:HTTPXMock):httpx_mock.add_exception(httpx.ReadTimeout("Unable to read within timeout"))withhttpx.Client()asclient:withpytest.raises(httpx.ReadTimeout):client.get("https://test_url")Note that default behavior is to send anhttpx.TimeoutExceptionin case no response can be found. You can then test this kind of exception this way:importhttpximportpytestfrompytest_httpximportHTTPXMockdeftest_timeout(httpx_mock:HTTPXMock):withhttpx.Client()asclient:withpytest.raises(httpx.TimeoutException):client.get("https://test_url")Check sent requestsThe best way to ensure the content of your requests is still to use thematch_headersand / ormatch_contentparameters when adding a response.
In the same spirit, ensuring that no request was issued does not necessarily require any code.In any case, you always have the ability to retrieve the requests that were issued.As in the following samples:importhttpxfrompytest_httpximportHTTPXMockdeftest_many_requests(httpx_mock:HTTPXMock):httpx_mock.add_response()withhttpx.Client()asclient:response1=client.get("https://test_url")response2=client.get("https://test_url")requests=httpx_mock.get_requests()deftest_single_request(httpx_mock:HTTPXMock):httpx_mock.add_response()withhttpx.Client()asclient:response=client.get("https://test_url")request=httpx_mock.get_request()deftest_no_request(httpx_mock:HTTPXMock):assertnothttpx_mock.get_request()How requests are selectedYou can add criteria so that requests will be returned only in case of a more specific matching.Matching on URLurlparameter can either be a string, a pythonre.Patterninstance or ahttpx.URLinstance.Matching is performed on the full URL, query parameters included.Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once.Matching on HTTP methodUsemethodparameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) of the requests to retrieve.methodparameter must be a string. It will be upper-cased, so it can be provided lower cased.Matching is performed on equality.Matching on proxy URLproxy_urlparameter can either be a string, a pythonre.Patterninstance or ahttpx.URLinstance.Matching is performed on the full proxy URL, query parameters included.Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once.Matching on HTTP headersUsematch_headersparameter to specify the HTTP headers executing the callback.Matching is performed on equality for each provided header.Matching on HTTP bodyUsematch_contentparameter to specify the full HTTP body executing the callback.Matching is performed on equality.Matching on HTTP JSON bodyUsematch_jsonparameter to specify the JSON decoded HTTP body executing the callback.Matching is performed on equality. You can however useunittest.mock.ANYto do partial matching.Note thatmatch_contentcannot be provided ifmatch_jsonis also provided.Do not mock some requestsBy default,pytest-httpxwill mock every request.But, for instance, in case you want to write integration tests with other servers, you might want to let some requests go through.To do so, you can use thenon_mocked_hostsfixture:importpytest@pytest.fixturedefnon_mocked_hosts()->list:return["my_local_test_host","my_other_test_host"]Every other requested hosts will be mocked as in the following exampleimportpytestimporthttpx@pytest.fixturedefnon_mocked_hosts()->list:return["my_local_test_host"]deftest_partial_mock(httpx_mock):httpx_mock.add_response()withhttpx.Client()asclient:# This request will NOT be mockedresponse1=client.get("https://www.my_local_test_host/sub?param=value")# This request will be mockedresponse2=client.get("https://test_url")Migrating to pytest-httpxHere is how to migrate from well-known testing libraries topytest-httpx.From responsesFeatureresponsespytest-httpxAdd a responseresponses.add()httpx_mock.add_response()Add a callbackresponses.add_callback()httpx_mock.add_callback()Retrieve requestsresponses.callshttpx_mock.get_requests()Add a response or a callbackUndocumented parameters means that they are unchanged betweenresponsesandpytest-httpx.
Below is a list of parameters that will require a change in your code.Parameterresponsespytest-httpxmethodmethod=responses.GETmethod="GET"body (as bytes)body=b"sample"content=b"sample"body (as str)body="sample"text="sample"status codestatus=201status_code=201headersadding_headers={"name": "value"}headers={"name": "value"}content-type headercontent_type="application/custom"headers={"content-type": "application/custom"}Match the full querymatch_querystring=TrueThe full query is always matched when providing theurlparameter.Sample adding a response withresponses:fromresponsesimportRequestsMockdeftest_response(responses:RequestsMock):responses.add(method=responses.GET,url="https://test_url",body=b"This is the response content",status=400,)Sample adding the same response withpytest-httpx:frompytest_httpximportHTTPXMockdeftest_response(httpx_mock:HTTPXMock):httpx_mock.add_response(method="GET",url="https://test_url",content=b"This is the response content",status_code=400,)From aioresponsesFeatureaioresponsespytest-httpxAdd a responseaioresponses.method()httpx_mock.add_response(method="METHOD")Add a callbackaioresponses.method()httpx_mock.add_callback(method="METHOD")Add a response or a callbackUndocumented parameters means that they are unchanged betweenresponsesandpytest-httpx.
Below is a list of parameters that will require a change in your code.Parameterresponsespytest-httpxbody (as bytes)body=b"sample"content=b"sample"body (as str)body="sample"text="sample"body (as JSON)payload=["sample"]json=["sample"]status codestatus=201status_code=201Sample adding a response withaioresponses:importpytestfromaioresponsesimportaioresponses@pytest.fixturedefmock_aioresponse():withaioresponses()asm:yieldmdeftest_response(mock_aioresponse):mock_aioresponse.get(url="https://test_url",body=b"This is the response content",status=400,)Sample adding the same response withpytest-httpx:deftest_response(httpx_mock):httpx_mock.add_response(method="GET",url="https://test_url",content=b"This is the response content",status_code=400,)