pytest-httpx

repository·develop·Indexed 19 days ago

https://github.com/colin-b/pytest_httpx

A pytest plugin that provides the `httpx_mock` fixture to mock HTTPX requests for both synchronous and asynchronous clients. It allows developers to define expected responses (JSON, custom bodies, multipart, etc.), simulate HTTPX exceptions, and verify sent requests. Key features include response customization, support for HTTP/2.0, dynamic responses via callbacks, and partial mocking capabilities via the `@pytest.mark.httpx_mock` marker.

Tokens
9.2K
Snippets
27
Records
29
Agent score
63%

What's inside pytest-httpx

  1. Overview of pytest-httpx capabilities

    develop

    pytest-httpx provides a comprehensive suite of tools for mocking HTTP interactions with httpx, including:

    • Response Customization: Add JSON bodies, custom bodies, multipart bodies (files, etc.), non-200 status codes, and custom headers.
    • Advanced Protocols: Support for HTTP/2.0.
    • Dynamic Behavior: Add dynamic responses and raise exceptions.
    • Verification: Check sent requests to ensure your code is making the expected calls.
    • Configuration: Control how many responses are registered, allow reusing responses, and skip mocking for specific hosts.
  2. How response selection works in pytest-httpx

    develop

    When multiple registered responses match a request, pytest-httpx selects the first one that has not yet been sent (based on registration order).

    • Exhaustion: If all matching responses have already been sent, the request will not be considered a match (unless can_send_already_matched_responses is enabled).
    • Specificity: You can provide matching criteria (URL, method, headers, etc.) to ensure specific responses are sent only for specific requests.
  3. Migrate from aioresponses to pytest-httpx

    develop

    If you are migrating from the aioresponses library, note the following mapping for core features and parameter names:

    Feature Mapping

    Featureaioresponsespytest-httpx
    Add a responseaioresponses.method()httpx_mock.add_response(method="METHOD")
    Add a callbackaioresponses.method()httpx_mock.add_callback(method="METHOD")

    Parameter Mapping

    Parameteraioresponsespytest-httpx
    body (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=201
    def test_response(httpx_mock):
        httpx_mock.add_response(
            method="GET",
            url="https://test_url",
            content=b"This is the response content",
            status_code=400,
        )
  4. Install and use pytest-httpx

    develop

    Once installed, the httpx_mock pytest fixture intercepts every httpx request and replies with user-provided responses. This allows you to mock HTTP interactions in your tests without making actual network calls.

    # Example usage pattern
    # (Note: Specific code implementation follows in subsequent sections)
    
    def test_my_request(httpx_mock):
        # Use httpx_mock to register responses
        pass
  5. Register responses for sync and async HTTPX requests

    develop

    Use the httpx_mock fixture to register responses for both synchronous httpx.Client and asynchronous httpx.AsyncClient requests. By default, add_response() creates a 200 (OK) response with an empty body.

    Note: If all registered responses are not used during a test, the test will fail at teardown unless the assert_all_responses_were_requested option is disabled.

    import pytest
    import httpx
    
    
    def test_something(httpx_mock):
        httpx_mock.add_response()
    
        with httpx.Client() as client:
            response = client.get("https://test_url")
    
    
    @pytest.mark.asyncio
    async def test_something_async(httpx_mock):
        httpx_mock.add_response()
    
        async with httpx.AsyncClient() as client:
            response = await client.get("https://test_url")
  6. How to allow unmocked requests (Partial Mocking)

    develop

    By default, pytest-httpx mocks every request. To allow certain requests to pass through to the real network (e.g., for integration testing with a local service), use the should_mock option in the httpx_mock marker. Provide a callable that accepts an httpx.Request and returns True to mock it or False to let it pass through.

    import pytest
    import httpx
    
    @pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "www.my_local_test_host")
    def test_partial_mock(httpx_mock):
        httpx_mock.add_response()
    
        with httpx.Client() as client:
            # This request will NOT be mocked
            response1 = client.get("https://www.my_local_test_host/sub?param=value")
            # This request will be mocked
            response2 = client.get("https://test_url")
  7. Configure httpx_mock behavior via markers

    develop

    The httpx_mock fixture behavior can be customized using the @pytest.mark.httpx_mock marker. You can apply these configurations at different levels:

    • Per test: Use @pytest.mark.httpx_mock(...) on the test function.
    • Per module: Define pytestmark = pytest.mark.httpx_mock(...) at the module level.
    • Whole test suite: Use pytest_collection_modifyitems in your root conftest.py to apply the marker to all items.
    import pytest
    
    # Per test
    @pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
    def test_something(httpx_mock):
        ...
    
    # Per module
    pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False)
  8. Migrate from responses to pytest-httpx

    develop

    If you are migrating from the responses library, note the following mapping for core features and parameter names:

    Feature Mapping

    Featureresponsespytest-httpx
    Add a responseresponses.add()httpx_mock.add_response()
    Add a callbackresponses.add_callback()httpx_mock.add_callback()
    Retrieve requestsresponses.callshttpx_mock.get_requests()

    Parameter Mapping

    Parameterresponsespytest-httpx
    methodmethod=responses.GETmethod="GET"
    body (as bytes)body=b"sample"content=b"sample"
    body (as str)body="sample"text="sample"
    status codestatus=201status_code=201
    headersadding_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 the url parameter.
    from pytest_httpx import HTTPXMock
    
    def test_response(httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url="https://test_url",
            content=b"This is the response content",
            status_code=400,
        )
  9. Simulate HTTPX exceptions

    develop

    To test how your code handles network or protocol errors, you can simulate exceptions in two ways:

    1. Use httpx_mock.add_exception(exception_instance) to raise a specific exception.
    2. Raise an exception directly inside a callback function.

    If no matching response is found for a request, pytest-httpx will raise an httpx.TimeoutException by default.

    import httpx
    import pytest
    from pytest_httpx import HTTPXMock
    
    def test_exception_raising(httpx_mock: HTTPXMock):
        httpx_mock.add_exception(httpx.ReadTimeout("Unable to read within timeout"))
    
        with httpx.Client() as client:
            with pytest.raises(httpx.ReadTimeout):
                client.get("https://test_url")
  10. Match responses by query parameters

    develop

    Use the match_params parameter to match specific query parameters.

    Constraints:

    • If match_params is used, the url parameter must not contain any query parameters.
    • All query parameters must be provided for a match, but you can use unittest.mock.ANY for partial matching.
    • For multiple values of the same parameter, use a list of values.
    import httpx
    from pytest_httpx import HTTPXMock
    from unittest.mock import ANY
    
    
    def test_partial_params_matching(httpx_mock: HTTPXMock):
        httpx_mock.add_response(url="https://test_url", match_params={"a": 1, "b": ANY})
    
        with httpx.Client() as client:
            response = client.get("https://test_url?a=1&b=2")
    
    
    def test_partial_multi_params_matching(httpx_mock: HTTPXMock):
        httpx_mock.add_response(url="https://test_url", match_params={"a": ["1", 3], "b": ["2", ANY]})
    
        with httpx.Client() as client:
            response = client.get("https://test_url?a=1&b=2&a=3&b=4")
  11. Retrieve and inspect sent requests

    develop

    You can inspect the requests that were actually issued by the client using httpx_mock.get_requests() (returns all requests) or httpx_mock.get_request() (returns the single most recent request).

    import httpx
    from pytest_httpx import HTTPXMock
    
    def test_many_requests(httpx_mock: HTTPXMock):
        httpx_mock.add_response()
    
        with httpx.Client() as client:
            response1 = client.get("https://test_url")
            response2 = client.get("https://test_url")
    
        requests = httpx_mock.get_requests()
    
    
    def test_single_request(httpx_mock: HTTPXMock):
        httpx_mock.add_response()
    
        with httpx.Client() as client:
            response = client.get("https://test_url")
    
        request = httpx_mock.get_request()
    
    
    def test_no_request(httpx_mock: HTTPXMock):
        assert not httpx_mock.get_request()
  12. Configure response status and headers

    develop

    Use these parameters to customize the HTTP metadata of the response:

    • status_code: An integer representing the HTTP status (e.g., 404).
    • headers: Custom headers. Supports a dict, a list of 2-tuples, or an httpx.Headers instance. To send cookies, set the set-cookie header using the key=value format.
    • http_version: A string specifying the protocol version (e.g., "HTTP/2.0").
    import httpx
    from pytest_httx import HTTPXMock
    
    
    def test_status_code(httpx_mock: HTTPXMock):
        httpx_mock.add_response(status_code=404)
    
        with httpx.Client() as client:
            assert client.get("https://test_url").status_code == 404
    
    
    def test_headers_as_str_dict(httpx_mock: HTTPXMock):
        httpx_mock.add_response(headers={"X-Header1": "Test value"})
    
        with httpx.Client() as client:
            assert client.get("https://test_url").headers["x-header1"] == "Test value"
    
    
    def test_cookie(httpx_mock: HTTPXMock):
        httpx_mock.add_response(headers={"set-cookie": "key=value"})
    
        with httpx.Client() as client:
            response = client.get("https://test_url")
        assert dict(response.cookies) == {"key": "value"}
    
    
    def test_http_version(httpx_mock: HTTPXMock):
        httpx_mock.add_response(http_version="HTTP/2.0")
    
        with httpx.Client() as client:
            assert client.get("https://test_url").http_version == "HTTP/2.0"