From 6fc81bc663a3fceb4c64c23dd6fb68e97dd68255 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 25 Nov 2020 14:36:32 +0000 Subject: [PATCH 1/5] Add httpx.MockTransport --- httpx/__init__.py | 2 + httpx/_transports/mock.py | 56 ++++++++++++++++++++++++ tests/client/test_async_client.py | 11 +++-- tests/client/test_auth.py | 73 +++++++++++++++++-------------- tests/client/test_client.py | 15 ++++--- tests/client/test_cookies.py | 13 +++--- tests/client/test_event_hooks.py | 19 +++++--- tests/client/test_headers.py | 19 ++++---- tests/client/test_queryparams.py | 3 +- tests/client/test_redirects.py | 51 +++++++++++---------- tests/test_multipart.py | 9 ++-- tests/utils.py | 55 ----------------------- 12 files changed, 169 insertions(+), 157 deletions(-) create mode 100644 httpx/_transports/mock.py diff --git a/httpx/__init__.py b/httpx/__init__.py index 489ffeae4f..8a6d9b32ce 100644 --- a/httpx/__init__.py +++ b/httpx/__init__.py @@ -36,6 +36,7 @@ from ._models import URL, Cookies, Headers, QueryParams, Request, Response from ._status_codes import StatusCode, codes from ._transports.asgi import ASGITransport +from ._transports.mock import MockTransport from ._transports.wsgi import WSGITransport __all__ = [ @@ -65,6 +66,7 @@ "InvalidURL", "Limits", "LocalProtocolError", + "MockTransport", "NetworkError", "options", "patch", diff --git a/httpx/_transports/mock.py b/httpx/_transports/mock.py new file mode 100644 index 0000000000..6f9ebc1e0f --- /dev/null +++ b/httpx/_transports/mock.py @@ -0,0 +1,56 @@ +from typing import Callable, List, Optional, Tuple + +import httpcore + +from .._models import Request + + +class MockTransport(httpcore.SyncHTTPTransport, httpcore.AsyncHTTPTransport): + def __init__(self, handler: Callable) -> None: + self.handler = handler + + def request( + self, + method: bytes, + url: Tuple[bytes, bytes, Optional[int], bytes], + headers: List[Tuple[bytes, bytes]] = None, + stream: httpcore.SyncByteStream = None, + ext: dict = None, + ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.SyncByteStream, dict]: + request = Request( + method=method, + url=url, + headers=headers, + stream=stream, + ) + request.read() + response = self.handler(request) + return ( + response.status_code, + response.headers.raw, + response.stream, + response.ext, + ) + + async def arequest( + self, + method: bytes, + url: Tuple[bytes, bytes, Optional[int], bytes], + headers: List[Tuple[bytes, bytes]] = None, + stream: httpcore.AsyncByteStream = None, + ext: dict = None, + ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream, dict]: + request = Request( + method=method, + url=url, + headers=headers, + stream=stream, + ) + await request.aread() + response = self.handler(request) + return ( + response.status_code, + response.headers.raw, + response.stream, + response.ext, + ) diff --git a/tests/client/test_async_client.py b/tests/client/test_async_client.py index 696f202cff..0cc9d8a4ff 100644 --- a/tests/client/test_async_client.py +++ b/tests/client/test_async_client.py @@ -5,7 +5,6 @@ import pytest import httpx -from tests.utils import MockTransport @pytest.mark.usefixtures("async_environment") @@ -247,7 +246,7 @@ def hello_world(request): @pytest.mark.usefixtures("async_environment") async def test_client_closed_state_using_implicit_open(): - client = httpx.AsyncClient(transport=MockTransport(hello_world)) + client = httpx.AsyncClient(transport=httpx.MockTransport(hello_world)) assert not client.is_closed await client.get("http://example.com") @@ -262,7 +261,7 @@ async def test_client_closed_state_using_implicit_open(): @pytest.mark.usefixtures("async_environment") async def test_client_closed_state_using_with_block(): - async with httpx.AsyncClient(transport=MockTransport(hello_world)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(hello_world)) as client: assert not client.is_closed await client.get("http://example.com") @@ -273,7 +272,7 @@ async def test_client_closed_state_using_with_block(): @pytest.mark.usefixtures("async_environment") async def test_deleting_unclosed_async_client_causes_warning(): - client = httpx.AsyncClient(transport=MockTransport(hello_world)) + client = httpx.AsyncClient(transport=httpx.MockTransport(hello_world)) await client.get("http://example.com") with pytest.warns(UserWarning): del client @@ -291,8 +290,8 @@ def mounted(request: httpx.Request) -> httpx.Response: @pytest.mark.usefixtures("async_environment") async def test_mounted_transport(): - transport = MockTransport(unmounted) - mounts = {"custom://": MockTransport(mounted)} + transport = httpx.MockTransport(unmounted) + mounts = {"custom://": httpx.MockTransport(mounted)} async with httpx.AsyncClient(transport=transport, mounts=mounts) as client: response = await client.get("https://www.example.com") diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 677fb9f619..c41afeff87 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -13,7 +13,6 @@ import httpx from httpx import URL, Auth, BasicAuth, DigestAuth, ProtocolError, Request, Response -from tests.utils import MockTransport from ..common import FIXTURES_DIR @@ -155,7 +154,7 @@ async def test_basic_auth() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -171,7 +170,9 @@ async def test_basic_auth_with_stream() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient(transport=MockTransport(app), auth=auth) as client: + async with httpx.AsyncClient( + transport=httpx.MockTransport(app), auth=auth + ) as client: async with client.stream("GET", url) as response: await response.aread() @@ -184,7 +185,7 @@ async def test_basic_auth_in_url() -> None: url = "https://tomchristie:password123@example.org/" app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url) assert response.status_code == 200 @@ -197,7 +198,9 @@ async def test_basic_auth_on_session() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient(transport=MockTransport(app), auth=auth) as client: + async with httpx.AsyncClient( + transport=httpx.MockTransport(app), auth=auth + ) as client: response = await client.get(url) assert response.status_code == 200 @@ -213,7 +216,7 @@ def auth(request: Request) -> Request: request.headers["Authorization"] = "Token 123" return request - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -226,7 +229,7 @@ async def test_netrc_auth() -> None: url = "http://netrcexample.org" app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url) assert response.status_code == 200 @@ -241,7 +244,7 @@ async def test_auth_header_has_priority_over_netrc() -> None: url = "http://netrcexample.org" app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, headers={"Authorization": "Override"}) assert response.status_code == 200 @@ -255,7 +258,7 @@ async def test_trust_env_auth() -> None: app = App() async with httpx.AsyncClient( - transport=MockTransport(app), trust_env=False + transport=httpx.MockTransport(app), trust_env=False ) as client: response = await client.get(url) @@ -263,7 +266,7 @@ async def test_trust_env_auth() -> None: assert response.json() == {"auth": None} async with httpx.AsyncClient( - transport=MockTransport(app), trust_env=True + transport=httpx.MockTransport(app), trust_env=True ) as client: response = await client.get(url) @@ -279,7 +282,9 @@ async def test_auth_disable_per_request() -> None: auth = ("tomchristie", "password123") app = App() - async with httpx.AsyncClient(transport=MockTransport(app), auth=auth) as client: + async with httpx.AsyncClient( + transport=httpx.MockTransport(app), auth=auth + ) as client: response = await client.get(url, auth=None) assert response.status_code == 200 @@ -299,7 +304,7 @@ async def test_auth_hidden_header() -> None: auth = ("example-username", "example-password") app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert "'authorization': '[secure]'" in str(response.request.headers) @@ -309,7 +314,7 @@ async def test_auth_hidden_header() -> None: async def test_auth_property() -> None: app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: assert client.auth is None client.auth = ("tomchristie", "password123") # type: ignore @@ -327,11 +332,11 @@ async def test_auth_invalid_type() -> None: with pytest.raises(TypeError): client = httpx.AsyncClient( - transport=MockTransport(app), + transport=httpx.MockTransport(app), auth="not a tuple, not a callable", # type: ignore ) - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: with pytest.raises(TypeError): await client.get(auth="not a tuple, not a callable") # type: ignore @@ -345,7 +350,7 @@ async def test_digest_auth_returns_no_auth_if_no_digest_header_in_response() -> auth = DigestAuth(username="tomchristie", password="password123") app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -359,7 +364,7 @@ def test_digest_auth_returns_no_auth_if_alternate_auth_scheme() -> None: auth_header = "Token ..." app = App(auth_header=auth_header, status_code=401) - client = httpx.Client(transport=MockTransport(app)) + client = httpx.Client(transport=httpx.MockTransport(app)) response = client.get(url, auth=auth) assert response.status_code == 401 @@ -374,7 +379,7 @@ async def test_digest_auth_200_response_including_digest_auth_header() -> None: auth_header = 'Digest realm="realm@host.com",qop="auth",nonce="abc",opaque="xyz"' app = App(auth_header=auth_header, status_code=200) - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -388,7 +393,7 @@ async def test_digest_auth_401_response_without_digest_auth_header() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = App(auth_header="", status_code=401) - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 401 @@ -417,7 +422,7 @@ async def test_digest_auth( auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(algorithm=algorithm) - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -448,7 +453,7 @@ async def test_digest_auth_no_specified_qop() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(qop="") - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -480,7 +485,7 @@ async def test_digest_auth_qop_including_spaces_and_auth_returns_auth(qop: str) auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(qop=qop) - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -493,7 +498,7 @@ async def test_digest_auth_qop_auth_int_not_implemented() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(qop="auth-int") - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: with pytest.raises(NotImplementedError): await client.get(url, auth=auth) @@ -504,7 +509,7 @@ async def test_digest_auth_qop_must_be_auth_or_auth_int() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(qop="not-auth") - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: with pytest.raises(ProtocolError): await client.get(url, auth=auth) @@ -515,7 +520,7 @@ async def test_digest_auth_incorrect_credentials() -> None: auth = DigestAuth(username="tomchristie", password="password123") app = DigestApp(send_response_after_attempt=2) - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 401 @@ -537,7 +542,7 @@ async def test_async_digest_auth_raises_protocol_error_on_malformed_header( auth = DigestAuth(username="tomchristie", password="password123") app = App(auth_header=auth_header, status_code=401) - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: with pytest.raises(ProtocolError): await client.get(url, auth=auth) @@ -556,7 +561,7 @@ def test_sync_digest_auth_raises_protocol_error_on_malformed_header( auth = DigestAuth(username="tomchristie", password="password123") app = App(auth_header=auth_header, status_code=401) - with httpx.Client(transport=MockTransport(app)) as client: + with httpx.Client(transport=httpx.MockTransport(app)) as client: with pytest.raises(ProtocolError): client.get(url, auth=auth) @@ -571,7 +576,7 @@ async def test_async_auth_history() -> None: auth = RepeatAuth(repeat=2) app = App(auth_header="abc") - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -597,7 +602,7 @@ def test_sync_auth_history() -> None: auth = RepeatAuth(repeat=2) app = App(auth_header="abc") - with httpx.Client(transport=MockTransport(app)) as client: + with httpx.Client(transport=httpx.MockTransport(app)) as client: response = client.get(url, auth=auth) assert response.status_code == 200 @@ -623,7 +628,7 @@ async def test_digest_auth_unavailable_streaming_body(): async def streaming_body(): yield b"Example request body" # pragma: nocover - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: with pytest.raises(httpx.StreamConsumed): await client.post(url, data=streaming_body(), auth=auth) @@ -638,7 +643,7 @@ async def test_async_auth_reads_response_body() -> None: auth = ResponseBodyAuth("xyz") app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -654,7 +659,7 @@ def test_sync_auth_reads_response_body() -> None: auth = ResponseBodyAuth("xyz") app = App() - with httpx.Client(transport=MockTransport(app)) as client: + with httpx.Client(transport=httpx.MockTransport(app)) as client: response = client.get(url, auth=auth) assert response.status_code == 200 @@ -672,7 +677,7 @@ async def test_async_auth() -> None: auth = SyncOrAsyncAuth() app = App() - async with httpx.AsyncClient(transport=MockTransport(app)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: response = await client.get(url, auth=auth) assert response.status_code == 200 @@ -687,7 +692,7 @@ def test_sync_auth() -> None: auth = SyncOrAsyncAuth() app = App() - with httpx.Client(transport=MockTransport(app)) as client: + with httpx.Client(transport=httpx.MockTransport(app)) as client: response = client.get(url, auth=auth) assert response.status_code == 200 diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 3675730b30..8d35b14639 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -5,7 +5,6 @@ import pytest import httpx -from tests.utils import MockTransport def test_get(server): @@ -282,7 +281,7 @@ def hello_world(request): def test_client_closed_state_using_implicit_open(): - client = httpx.Client(transport=MockTransport(hello_world)) + client = httpx.Client(transport=httpx.MockTransport(hello_world)) assert not client.is_closed client.get("http://example.com") @@ -296,7 +295,7 @@ def test_client_closed_state_using_implicit_open(): def test_client_closed_state_using_with_block(): - with httpx.Client(transport=MockTransport(hello_world)) as client: + with httpx.Client(transport=httpx.MockTransport(hello_world)) as client: assert not client.is_closed client.get("http://example.com") @@ -320,7 +319,9 @@ def test_raw_client_header(): url = "http://example.org/echo_headers" headers = {"Example-Header": "example-value"} - client = httpx.Client(transport=MockTransport(echo_raw_headers), headers=headers) + client = httpx.Client( + transport=httpx.MockTransport(echo_raw_headers), headers=headers + ) response = client.get(url) assert response.status_code == 200 @@ -345,8 +346,8 @@ def mounted(request: httpx.Request) -> httpx.Response: def test_mounted_transport(): - transport = MockTransport(unmounted) - mounts = {"custom://": MockTransport(mounted)} + transport = httpx.MockTransport(unmounted) + mounts = {"custom://": httpx.MockTransport(mounted)} client = httpx.Client(transport=transport, mounts=mounts) @@ -360,7 +361,7 @@ def test_mounted_transport(): def test_all_mounted_transport(): - mounts = {"all://": MockTransport(mounted)} + mounts = {"all://": httpx.MockTransport(mounted)} client = httpx.Client(mounts=mounts) diff --git a/tests/client/test_cookies.py b/tests/client/test_cookies.py index feb26ac436..fe9125fa06 100644 --- a/tests/client/test_cookies.py +++ b/tests/client/test_cookies.py @@ -1,7 +1,6 @@ from http.cookiejar import Cookie, CookieJar import httpx -from tests.utils import MockTransport def get_and_set_cookies(request: httpx.Request) -> httpx.Response: @@ -21,7 +20,7 @@ def test_set_cookie() -> None: url = "http://example.org/echo_cookies" cookies = {"example-name": "example-value"} - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) response = client.get(url, cookies=cookies) assert response.status_code == 200 @@ -56,7 +55,7 @@ def test_set_cookie_with_cookiejar() -> None: ) cookies.set_cookie(cookie) - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) response = client.get(url, cookies=cookies) assert response.status_code == 200 @@ -91,7 +90,7 @@ def test_setting_client_cookies_to_cookiejar() -> None: ) cookies.set_cookie(cookie) - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) client.cookies = cookies # type: ignore response = client.get(url) @@ -108,7 +107,7 @@ def test_set_cookie_with_cookies_model() -> None: cookies = httpx.Cookies() cookies["example-name"] = "example-value" - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) response = client.get(url, cookies=cookies) assert response.status_code == 200 @@ -118,7 +117,7 @@ def test_set_cookie_with_cookies_model() -> None: def test_get_cookie() -> None: url = "http://example.org/set_cookie" - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) response = client.get(url) assert response.status_code == 200 @@ -130,7 +129,7 @@ def test_cookie_persistence() -> None: """ Ensure that Client instances persist cookies between requests. """ - client = httpx.Client(transport=MockTransport(get_and_set_cookies)) + client = httpx.Client(transport=httpx.MockTransport(get_and_set_cookies)) response = client.get("http://example.org/echo_cookies") assert response.status_code == 200 diff --git a/tests/client/test_event_hooks.py b/tests/client/test_event_hooks.py index a81f31e1e5..24dc8dd70b 100644 --- a/tests/client/test_event_hooks.py +++ b/tests/client/test_event_hooks.py @@ -1,7 +1,6 @@ import pytest import httpx -from tests.utils import MockTransport def app(request: httpx.Request) -> httpx.Response: @@ -25,7 +24,9 @@ def on_response(response): event_hooks = {"request": [on_request], "response": [on_response]} - with httpx.Client(event_hooks=event_hooks, transport=MockTransport(app)) as http: + with httpx.Client( + event_hooks=event_hooks, transport=httpx.MockTransport(app) + ) as http: http.get("http://127.0.0.1:8000/", auth=("username", "password")) assert events == [ @@ -53,7 +54,9 @@ def raise_on_4xx_5xx(response): event_hooks = {"response": [raise_on_4xx_5xx]} - with httpx.Client(event_hooks=event_hooks, transport=MockTransport(app)) as http: + with httpx.Client( + event_hooks=event_hooks, transport=httpx.MockTransport(app) + ) as http: try: http.get("http://127.0.0.1:8000/status/400") except httpx.HTTPStatusError as exc: @@ -73,7 +76,7 @@ async def on_response(response): event_hooks = {"request": [on_request], "response": [on_response]} async with httpx.AsyncClient( - event_hooks=event_hooks, transport=MockTransport(app) + event_hooks=event_hooks, transport=httpx.MockTransport(app) ) as http: await http.get("http://127.0.0.1:8000/", auth=("username", "password")) @@ -104,7 +107,7 @@ async def raise_on_4xx_5xx(response): event_hooks = {"response": [raise_on_4xx_5xx]} async with httpx.AsyncClient( - event_hooks=event_hooks, transport=MockTransport(app) + event_hooks=event_hooks, transport=httpx.MockTransport(app) ) as http: try: await http.get("http://127.0.0.1:8000/status/400") @@ -127,7 +130,9 @@ def on_response(response): event_hooks = {"request": [on_request], "response": [on_response]} - with httpx.Client(event_hooks=event_hooks, transport=MockTransport(app)) as http: + with httpx.Client( + event_hooks=event_hooks, transport=httpx.MockTransport(app) + ) as http: http.get("http://127.0.0.1:8000/redirect", auth=("username", "password")) assert events == [ @@ -166,7 +171,7 @@ async def on_response(response): event_hooks = {"request": [on_request], "response": [on_response]} async with httpx.AsyncClient( - event_hooks=event_hooks, transport=MockTransport(app) + event_hooks=event_hooks, transport=httpx.MockTransport(app) ) as http: await http.get("http://127.0.0.1:8000/redirect", auth=("username", "password")) diff --git a/tests/client/test_headers.py b/tests/client/test_headers.py index 556cd1df14..ba862fd521 100755 --- a/tests/client/test_headers.py +++ b/tests/client/test_headers.py @@ -3,7 +3,6 @@ import pytest import httpx -from tests.utils import MockTransport def echo_headers(request: httpx.Request) -> httpx.Response: @@ -18,7 +17,7 @@ def test_client_header(): url = "http://example.org/echo_headers" headers = {"Example-Header": "example-value"} - client = httpx.Client(transport=MockTransport(echo_headers), headers=headers) + client = httpx.Client(transport=httpx.MockTransport(echo_headers), headers=headers) response = client.get(url) assert response.status_code == 200 @@ -38,7 +37,9 @@ def test_header_merge(): url = "http://example.org/echo_headers" client_headers = {"User-Agent": "python-myclient/0.2.1"} request_headers = {"X-Auth-Token": "FooBarBazToken"} - client = httpx.Client(transport=MockTransport(echo_headers), headers=client_headers) + client = httpx.Client( + transport=httpx.MockTransport(echo_headers), headers=client_headers + ) response = client.get(url, headers=request_headers) assert response.status_code == 200 @@ -58,7 +59,9 @@ def test_header_merge_conflicting_headers(): url = "http://example.org/echo_headers" client_headers = {"X-Auth-Token": "FooBar"} request_headers = {"X-Auth-Token": "BazToken"} - client = httpx.Client(transport=MockTransport(echo_headers), headers=client_headers) + client = httpx.Client( + transport=httpx.MockTransport(echo_headers), headers=client_headers + ) response = client.get(url, headers=request_headers) assert response.status_code == 200 @@ -76,7 +79,7 @@ def test_header_merge_conflicting_headers(): def test_header_update(): url = "http://example.org/echo_headers" - client = httpx.Client(transport=MockTransport(echo_headers)) + client = httpx.Client(transport=httpx.MockTransport(echo_headers)) first_response = client.get(url) client.headers.update( {"User-Agent": "python-myclient/0.2.1", "Another-Header": "AThing"} @@ -113,7 +116,7 @@ def test_remove_default_header(): """ url = "http://example.org/echo_headers" - client = httpx.Client(transport=MockTransport(echo_headers)) + client = httpx.Client(transport=httpx.MockTransport(echo_headers)) del client.headers["User-Agent"] response = client.get(url) @@ -143,7 +146,7 @@ def test_host_with_auth_and_port_in_url(): """ url = "http://username:password@example.org:80/echo_headers" - client = httpx.Client(transport=MockTransport(echo_headers)) + client = httpx.Client(transport=httpx.MockTransport(echo_headers)) response = client.get(url) assert response.status_code == 200 @@ -166,7 +169,7 @@ def test_host_with_non_default_port_in_url(): """ url = "http://username:password@example.org:123/echo_headers" - client = httpx.Client(transport=MockTransport(echo_headers)) + client = httpx.Client(transport=httpx.MockTransport(echo_headers)) response = client.get(url) assert response.status_code == 200 diff --git a/tests/client/test_queryparams.py b/tests/client/test_queryparams.py index 6d3a9d5b5d..e5acb0ba20 100644 --- a/tests/client/test_queryparams.py +++ b/tests/client/test_queryparams.py @@ -1,5 +1,4 @@ import httpx -from tests.utils import MockTransport def hello_world(request: httpx.Request) -> httpx.Response: @@ -28,7 +27,7 @@ def test_client_queryparams_echo(): client_queryparams = "first=str" request_queryparams = {"second": "dict"} client = httpx.Client( - transport=MockTransport(hello_world), params=client_queryparams + transport=httpx.MockTransport(hello_world), params=client_queryparams ) response = client.get(url, params=request_queryparams) diff --git a/tests/client/test_redirects.py b/tests/client/test_redirects.py index 99610c7715..84d371e9fa 100644 --- a/tests/client/test_redirects.py +++ b/tests/client/test_redirects.py @@ -2,7 +2,6 @@ import pytest import httpx -from tests.utils import MockTransport def redirects(request: httpx.Request) -> httpx.Response: @@ -116,7 +115,7 @@ def redirects(request: httpx.Request) -> httpx.Response: def test_redirect_301(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.post("https://example.org/redirect_301") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -124,7 +123,7 @@ def test_redirect_301(): def test_redirect_302(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.post("https://example.org/redirect_302") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -132,7 +131,7 @@ def test_redirect_302(): def test_redirect_303(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("https://example.org/redirect_303") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -140,7 +139,7 @@ def test_redirect_303(): def test_next_request(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) request = client.build_request("POST", "https://example.org/redirect_303") response = client.send(request, allow_redirects=False) assert response.status_code == httpx.codes.SEE_OTHER @@ -155,7 +154,7 @@ def test_next_request(): @pytest.mark.usefixtures("async_environment") async def test_async_next_request(): - async with httpx.AsyncClient(transport=MockTransport(redirects)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(redirects)) as client: request = client.build_request("POST", "https://example.org/redirect_303") response = await client.send(request, allow_redirects=False) assert response.status_code == httpx.codes.SEE_OTHER @@ -172,7 +171,7 @@ def test_head_redirect(): """ Contrary to Requests, redirects remain enabled by default for HEAD requests. """ - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.head("https://example.org/redirect_302") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -182,7 +181,7 @@ def test_head_redirect(): def test_relative_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("https://example.org/relative_redirect") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -191,7 +190,7 @@ def test_relative_redirect(): def test_malformed_redirect(): # https://github.com/encode/httpx/issues/771 - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("http://example.org/malformed_redirect") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org:443/" @@ -199,13 +198,13 @@ def test_malformed_redirect(): def test_invalid_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) with pytest.raises(httpx.RemoteProtocolError): client.get("http://example.org/invalid_redirect") def test_no_scheme_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("https://example.org/no_scheme_redirect") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" @@ -213,7 +212,7 @@ def test_no_scheme_redirect(): def test_fragment_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("https://example.org/relative_redirect#fragment") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/#fragment" @@ -221,7 +220,7 @@ def test_fragment_redirect(): def test_multiple_redirects(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) response = client.get("https://example.org/multiple_redirects?count=20") assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/multiple_redirects" @@ -234,25 +233,25 @@ def test_multiple_redirects(): @pytest.mark.usefixtures("async_environment") async def test_async_too_many_redirects(): - async with httpx.AsyncClient(transport=MockTransport(redirects)) as client: + async with httpx.AsyncClient(transport=httpx.MockTransport(redirects)) as client: with pytest.raises(httpx.TooManyRedirects): await client.get("https://example.org/multiple_redirects?count=21") def test_sync_too_many_redirects(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) with pytest.raises(httpx.TooManyRedirects): client.get("https://example.org/multiple_redirects?count=21") def test_redirect_loop(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) with pytest.raises(httpx.TooManyRedirects): client.get("https://example.org/redirect_loop") def test_cross_domain_redirect_with_auth_header(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.com/cross_domain" headers = {"Authorization": "abc"} response = client.get(url, headers=headers) @@ -261,7 +260,7 @@ def test_cross_domain_redirect_with_auth_header(): def test_cross_domain_redirect_with_auth(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.com/cross_domain" response = client.get(url, auth=("user", "pass")) assert response.url == "https://example.org/cross_domain_target" @@ -269,7 +268,7 @@ def test_cross_domain_redirect_with_auth(): def test_same_domain_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.org/cross_domain" headers = {"Authorization": "abc"} response = client.get(url, headers=headers) @@ -281,7 +280,7 @@ def test_body_redirect(): """ A 308 redirect should preserve the request body. """ - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.org/redirect_body" content = b"Example request body" response = client.post(url, content=content) @@ -294,7 +293,7 @@ def test_no_body_redirect(): """ A 303 redirect should remove the request body. """ - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.org/redirect_no_body" content = b"Example request body" response = client.post(url, content=content) @@ -304,7 +303,7 @@ def test_no_body_redirect(): def test_can_stream_if_no_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.org/redirect_301" with client.stream("GET", url, allow_redirects=False) as response: assert not response.is_closed @@ -313,7 +312,7 @@ def test_can_stream_if_no_redirect(): def test_cannot_redirect_streaming_body(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.org/redirect_body" def streaming_body(): @@ -324,7 +323,7 @@ def streaming_body(): def test_cross_subdomain_redirect(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) url = "https://example.com/cross_subdomain" response = client.get(url) assert response.url == "https://www.example.org/cross_subdomain" @@ -364,7 +363,7 @@ def cookie_sessions(request: httpx.Request) -> httpx.Response: def test_redirect_cookie_behavior(): - client = httpx.Client(transport=MockTransport(cookie_sessions)) + client = httpx.Client(transport=httpx.MockTransport(cookie_sessions)) # The client is not logged in. response = client.get("https://example.com/") @@ -393,7 +392,7 @@ def test_redirect_cookie_behavior(): def test_redirect_custom_scheme(): - client = httpx.Client(transport=MockTransport(redirects)) + client = httpx.Client(transport=httpx.MockTransport(redirects)) with pytest.raises(httpx.UnsupportedProtocol) as e: client.post("https://example.org/redirect_custom_scheme") assert str(e.value) == "Scheme 'market' not supported." diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 8852c9f905..94813932a8 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -9,7 +9,6 @@ import httpx from httpx._content import encode_request from httpx._utils import format_form_param -from tests.utils import MockTransport def echo_request_content(request: httpx.Request) -> httpx.Response: @@ -18,7 +17,7 @@ def echo_request_content(request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize(("value,output"), (("abc", b"abc"), (b"abc", b"abc"))) def test_multipart(value, output): - client = httpx.Client(transport=MockTransport(echo_request_content)) + client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) # Test with a single-value 'data' argument, and a plain file 'files' argument. data = {"text": value} @@ -44,7 +43,7 @@ def test_multipart(value, output): @pytest.mark.parametrize(("key"), (b"abc", 1, 2.3, None)) def test_multipart_invalid_key(key): - client = httpx.Client(transport=MockTransport(echo_request_content)) + client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) data = {key: "abc"} files = {"file": io.BytesIO(b"")} @@ -60,7 +59,7 @@ def test_multipart_invalid_key(key): @pytest.mark.parametrize(("value"), (1, 2.3, None, [None, "abc"], {None: "abc"})) def test_multipart_invalid_value(value): - client = httpx.Client(transport=MockTransport(echo_request_content)) + client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) data = {"text": value} files = {"file": io.BytesIO(b"")} @@ -70,7 +69,7 @@ def test_multipart_invalid_value(value): def test_multipart_file_tuple(): - client = httpx.Client(transport=MockTransport(echo_request_content)) + client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) # Test with a list of values 'data' argument, # and a tuple style 'files' argument. diff --git a/tests/utils.py b/tests/utils.py index 75157dee7e..e2636a535c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,11 +1,7 @@ import contextlib import logging import os -from typing import Callable, List, Optional, Tuple -import httpcore - -import httpx from httpx import _utils @@ -22,54 +18,3 @@ def override_log_level(log_level: str): finally: # Reset the logger so we don't have verbose output in all unit tests logging.getLogger("httpx").handlers = [] - - -class MockTransport(httpcore.SyncHTTPTransport, httpcore.AsyncHTTPTransport): - def __init__(self, handler: Callable) -> None: - self.handler = handler - - def request( - self, - method: bytes, - url: Tuple[bytes, bytes, Optional[int], bytes], - headers: List[Tuple[bytes, bytes]] = None, - stream: httpcore.SyncByteStream = None, - ext: dict = None, - ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.SyncByteStream, dict]: - request = httpx.Request( - method=method, - url=url, - headers=headers, - stream=stream, - ) - request.read() - response = self.handler(request) - return ( - response.status_code, - response.headers.raw, - response.stream, - response.ext, - ) - - async def arequest( - self, - method: bytes, - url: Tuple[bytes, bytes, Optional[int], bytes], - headers: List[Tuple[bytes, bytes]] = None, - stream: httpcore.AsyncByteStream = None, - ext: dict = None, - ) -> Tuple[int, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream, dict]: - request = httpx.Request( - method=method, - url=url, - headers=headers, - stream=stream, - ) - await request.aread() - response = self.handler(request) - return ( - response.status_code, - response.headers.raw, - response.stream, - response.ext, - ) From 4a4f8557dd1eebbc76b74bbf16b839664d16c152 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 25 Nov 2020 14:58:41 +0000 Subject: [PATCH 2/5] Add docs on MockTransport --- docs/advanced.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/advanced.md b/docs/advanced.md index 3e009b1aac..cd57154e31 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -1059,6 +1059,28 @@ Which we can use in the same way: {"text": "Hello, world!"} ``` +### Mock transports + +During testing it can often be useful to be able to mock out a transport, +and return pre-determined responses, rather than making actual network requests. + +The `httpx.MockTransport` class accepts a handler function, which can be used +to map requests onto pre-determined responses: + +```python +def handler(request): + return httpx.Response(200, json={"text": "Hello, world!"}) + + +# Switch to a mock transport, if the TESTING environment variable is set. +if os.environ['TESTING'].upper() == "TRUE": + transport = httpx.MockTransport(handler) +else: + transport = httpx.HTTPTransport() + +client = httpx.Client(transport=transport) +``` + ### Mounting transports You can also mount transports against given schemes or domains, to control From 74518cc64f5c584324b46cf532995284515a22aa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 25 Nov 2020 14:59:55 +0000 Subject: [PATCH 3/5] Add pointer to RESPX --- docs/advanced.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/advanced.md b/docs/advanced.md index cd57154e31..42260f58d9 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -1081,6 +1081,9 @@ else: client = httpx.Client(transport=transport) ``` +For more advanced use-cases you might want to take a look at [the third-party +mocking library, RESPX](https://lundberg.github.io/respx/). + ### Mounting transports You can also mount transports against given schemes or domains, to control From eeded260539263c60e062585f98d0498aedb86b4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 25 Nov 2020 15:03:08 +0000 Subject: [PATCH 4/5] Add note on pytest-httpx --- docs/advanced.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced.md b/docs/advanced.md index 42260f58d9..c59e5900a2 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -1081,8 +1081,8 @@ else: client = httpx.Client(transport=transport) ``` -For more advanced use-cases you might want to take a look at [the third-party -mocking library, RESPX](https://lundberg.github.io/respx/). +For more advanced use-cases you might want to take a look at either [the third-party +mocking library, RESPX](https://lundberg.github.io/respx/), or the [pytest-httpx library](https://github.com/Colin-b/pytest_httpx). ### Mounting transports From 8b393af733cd12fd6a840c48a7e7798ad95d484b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 27 Nov 2020 11:44:43 +0000 Subject: [PATCH 5/5] Tweak existing docs example to use 'httpx.MockTransport' --- docs/advanced.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/advanced.md b/docs/advanced.md index c59e5900a2..abe037da20 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -1123,7 +1123,10 @@ Mocking requests to a given domain: ```python # All requests to "example.org" should be mocked out. # Other requests occur as usual. -mounts = {"all://example.org": MockTransport()} +def handler(request): + return httpx.Response(200, json={"text": "Hello, World!"}) + +mounts = {"all://example.org": httpx.MockTransport(handler)} client = httpx.Client(mounts=mounts) ```