Giter Club home page Giter Club logo

pytest_httpx's Introduction

Send responses to HTTPX using pytest

pypi version Build status Coverage Code style: black Number of tests Number of downloads

Version 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_mock pytest fixture will make sure every httpx request will be replied to with user provided responses.

Add responses

You can register responses for both sync and async HTTPX requests.

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")

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 the assert_all_responses_were_requested fixture:

import pytest

@pytest.fixture
def assert_all_responses_were_requested() -> bool:
    return False

Default response is a HTTP/1.1 200 (OK) without any body.

How response is selected

In 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 URL

url parameter can either be a string, a python re.Pattern instance or a httpx.URL instance.

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.

import httpx
from pytest_httpx import HTTPXMock


def test_url(httpx_mock: HTTPXMock):
    httpx_mock.add_response(url="https://test_url?a=1&b=2")

    with httpx.Client() as client:
        response1 = client.delete("https://test_url?a=1&b=2")
        response2 = client.get("https://test_url?b=2&a=1")

Matching on HTTP method

Use method parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) to reply to.

method parameter must be a string. It will be upper-cased, so it can be provided lower cased.

Matching is performed on equality.

import httpx
from pytest_httpx import HTTPXMock


def test_post(httpx_mock: HTTPXMock):
    httpx_mock.add_response(method="POST")

    with httpx.Client() as client:
        response = client.post("https://test_url")


def test_put(httpx_mock: HTTPXMock):
    httpx_mock.add_response(method="PUT")

    with httpx.Client() as client:
        response = client.put("https://test_url")


def test_delete(httpx_mock: HTTPXMock):
    httpx_mock.add_response(method="DELETE")

    with httpx.Client() as client:
        response = client.delete("https://test_url")


def test_patch(httpx_mock: HTTPXMock):
    httpx_mock.add_response(method="PATCH")

    with httpx.Client() as client:
        response = client.patch("https://test_url")


def test_head(httpx_mock: HTTPXMock):
    httpx_mock.add_response(method="HEAD")

    with httpx.Client() as client:
        response = client.head("https://test_url")
    

Matching on proxy URL

proxy_url parameter can either be a string, a python re.Pattern instance or a httpx.URL instance.

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.

import httpx
from pytest_httpx import HTTPXMock


def test_proxy_url(httpx_mock: HTTPXMock):
    httpx_mock.add_response(proxy_url="http://test_proxy_url?b=1&a=2")

    with httpx.Client(proxy="http://test_proxy_url?a=2&b=1") as client:
        response = client.get("https://test_url")

Matching on HTTP headers

Use match_headers parameter to specify the HTTP headers to reply to.

Matching is performed on equality for each provided header.

import httpx
from pytest_httpx import HTTPXMock


def test_headers_matching(httpx_mock: HTTPXMock):
    httpx_mock.add_response(match_headers={'User-Agent': 'python-httpx/0.25.0'})

    with httpx.Client() as client:
        response = client.get("https://test_url")

Matching on HTTP body

Use match_content parameter to specify the full HTTP body to reply to.

Matching is performed on equality.

import httpx
from pytest_httpx import HTTPXMock


def test_content_matching(httpx_mock: HTTPXMock):
    httpx_mock.add_response(match_content=b"This is the body")

    with httpx.Client() as client:
        response = client.post("https://test_url", content=b"This is the body")
Matching on HTTP JSON body

Use match_json parameter to specify the JSON decoded HTTP body to reply to.

Matching is performed on equality. You can however use unittest.mock.ANY to do partial matching.

import httpx
from pytest_httpx import HTTPXMock
from unittest.mock import ANY

def test_json_matching(httpx_mock: HTTPXMock):
    httpx_mock.add_response(match_json={"a": "json", "b": 2})

    with httpx.Client() as client:
        response = client.post("https://test_url", json={"a": "json", "b": 2})

        
def test_partial_json_matching(httpx_mock: HTTPXMock):
    httpx_mock.add_response(match_json={"a": "json", "b": ANY})

    with httpx.Client() as client:
        response = client.post("https://test_url", json={"a": "json", "b": 2})

Note that match_content cannot be provided if match_json is also provided.

Add JSON response

Use json parameter to add a JSON response using python values.

import httpx
from pytest_httpx import HTTPXMock


def test_json(httpx_mock: HTTPXMock):
    httpx_mock.add_response(json=[{"key1": "value1", "key2": "value2"}])

    with httpx.Client() as client:
        assert client.get("https://test_url").json() == [{"key1": "value1", "key2": "value2"}]
    

Note that the content-type header will be set to application/json by default in the response.

Reply with custom body

Use text parameter to reply with a custom body by providing UTF-8 encoded string.

import httpx
from pytest_httpx import HTTPXMock


def test_str_body(httpx_mock: HTTPXMock):
    httpx_mock.add_response(text="This is my UTF-8 content")

    with httpx.Client() as client:
        assert client.get("https://test_url").text == "This is my UTF-8 content"

Use content parameter to reply with a custom body by providing bytes.

import httpx
from pytest_httpx import HTTPXMock


def test_bytes_body(httpx_mock: HTTPXMock):
    httpx_mock.add_response(content=b"This is my bytes content")

    with httpx.Client() as client:
        assert client.get("https://test_url").content == b"This is my bytes content"
    

Use html parameter to reply with a custom body by providing UTF-8 encoded string.

import httpx
from pytest_httpx import HTTPXMock


def test_html_body(httpx_mock: HTTPXMock):
    httpx_mock.add_response(html="<body>This is <p> HTML content</body>")

    with httpx.Client() as client:
        assert client.get("https://test_url").text == "<body>This is <p> HTML content</body>"

Reply by streaming chunks

Use stream parameter to stream chunks that you specify.

import httpx
import pytest
from pytest_httpx import HTTPXMock, IteratorStream

def test_sync_streaming(httpx_mock: HTTPXMock):
    httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"]))

    with httpx.Client() as client:
        with client.stream(method="GET", url="https://test_url") as response:
            assert list(response.iter_raw()) == [b"part 1", b"part 2"]


@pytest.mark.asyncio
async def test_async_streaming(httpx_mock: HTTPXMock):
    httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"]))

    async with httpx.AsyncClient() as client:
        async with client.stream(method="GET", url="https://test_url") as response:
            assert [part async for part in response.aiter_raw()] == [b"part 1", b"part 2"]
    

Add multipart response

Use the httpx MultipartStream via the stream parameter to send a multipart response.

Reach out to httpx developers if you need this publicly exposed as this is not a standard use case.

import httpx
from httpx._multipart import MultipartStream
from pytest_httpx import HTTPXMock


def test_multipart_body(httpx_mock: HTTPXMock):
    httpx_mock.add_response(stream=MultipartStream(data={"key1": "value1"}, files={"file1": b"content of file 1"}, boundary=b"2256d3a36d2a61a1eba35a22bee5c74a"))

    with httpx.Client() as client:
        assert client.get("https://test_url").text == '''--2256d3a36d2a61a1eba35a22bee5c74a\r
Content-Disposition: form-data; name="key1"\r
\r
value1\r
--2256d3a36d2a61a1eba35a22bee5c74a\r
Content-Disposition: form-data; name="file1"; filename="upload"\r
Content-Type: application/octet-stream\r
\r
content of file 1\r
--2256d3a36d2a61a1eba35a22bee5c74a--\r
'''
    

Add non 200 response

Use status_code parameter to specify the HTTP status code of the response.

import httpx
from pytest_httpx 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

Reply with custom headers

Use headers parameter 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 a httpx.Header instance.

import httpx
from pytest_httpx import HTTPXMock


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_headers_as_str_tuple_list(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_headers_as_httpx_headers(httpx_mock: HTTPXMock):
    httpx_mock.add_response(headers=httpx.Headers({b"X-Header1": b"Test value"}))

    with httpx.Client() as client:
        assert client.get("https://test_url").headers["x-header1"] == "Test value"

Reply with cookies

Cookies are sent in the set-cookie HTTP header.

You can then send cookies in the response by setting the set-cookie header with the value following key=value format.

import httpx
from pytest_httpx import HTTPXMock


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_cookies(httpx_mock: HTTPXMock):
    httpx_mock.add_response(headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")])

    with httpx.Client() as client:
        response = client.get("https://test_url")
    assert dict(response.cookies) == {"key": "value", "key2": "value2"}

Add HTTP/2.0 response

Use http_version parameter to specify the HTTP protocol version of the response.

import httpx
from pytest_httpx import HTTPXMock


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"

Add callbacks

You can perform custom manipulation upon request reception by registering callbacks.

Callback should expect one parameter, the received httpx.Request.

If all callbacks are not executed during test execution, the test case will fail at teardown.

This behavior can be disabled thanks to the assert_all_responses_were_requested fixture:

import pytest

@pytest.fixture
def assert_all_responses_were_requested() -> bool:
    return False

Note that callbacks are considered as responses, and thus are selected the same way.

Dynamic responses

Callback should return a httpx.Response.

import httpx
from pytest_httpx import HTTPXMock


def test_dynamic_response(httpx_mock: HTTPXMock):
    def custom_response(request: httpx.Request):
        return httpx.Response(
            status_code=200, json={"url": str(request.url)},
        )

    httpx_mock.add_callback(custom_response)

    with httpx.Client() as client:
        response = client.get("https://test_url")
        assert response.json() == {"url": "https://test_url"}

Alternatively, callbacks can also be asynchronous.

As in the following sample simulating network latency on some responses only.

import asyncio
import httpx
import pytest
from pytest_httpx import HTTPXMock


@pytest.mark.asyncio
async def test_dynamic_async_response(httpx_mock: HTTPXMock):
    async def simulate_network_latency(request: httpx.Request):
        await asyncio.sleep(1)
        return httpx.Response(
            status_code=200, json={"url": str(request.url)},
        )

    httpx_mock.add_callback(simulate_network_latency)
    httpx_mock.add_response()

    async with httpx.AsyncClient() as client:
        responses = await asyncio.gather(
            # Response will be received after one second
            client.get("https://test_url"),
            # Response will instantly be received (1 second before the first request)
            client.get("https://test_url")
        )

Raising exceptions

You can simulate HTTPX exception throwing by raising an exception in your callback or use httpx_mock.add_exception with the exception instance.

This can be useful if you want to assert that your code handles HTTPX exceptions properly.

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")

Note that default behavior is to send an httpx.TimeoutException in case no response can be found. You can then test this kind of exception this way:

import httpx
import pytest
from pytest_httpx import HTTPXMock


def test_timeout(httpx_mock: HTTPXMock):
    with httpx.Client() as client:
        with pytest.raises(httpx.TimeoutException):
            client.get("https://test_url")

Check sent requests

The best way to ensure the content of your requests is still to use the match_headers and / or match_content parameters 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:

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()

How requests are selected

You can add criteria so that requests will be returned only in case of a more specific matching.

Matching on URL

url parameter can either be a string, a python re.Pattern instance or a httpx.URL instance.

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 method

Use method parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) of the requests to retrieve.

method parameter must be a string. It will be upper-cased, so it can be provided lower cased.

Matching is performed on equality.

Matching on proxy URL

proxy_url parameter can either be a string, a python re.Pattern instance or a httpx.URL instance.

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 headers

Use match_headers parameter to specify the HTTP headers executing the callback.

Matching is performed on equality for each provided header.

Matching on HTTP body

Use match_content parameter to specify the full HTTP body executing the callback.

Matching is performed on equality.

Matching on HTTP JSON body

Use match_json parameter to specify the JSON decoded HTTP body executing the callback.

Matching is performed on equality. You can however use unittest.mock.ANY to do partial matching.

Note that match_content cannot be provided if match_json is also provided.

Do not mock some requests

By default, pytest-httpx will 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 the non_mocked_hosts fixture:

import pytest

@pytest.fixture
def non_mocked_hosts() -> list:
    return ["my_local_test_host", "my_other_test_host"]

Every other requested hosts will be mocked as in the following example

import pytest
import httpx

@pytest.fixture
def non_mocked_hosts() -> list:
    return ["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")

Migrating to pytest-httpx

Here is how to migrate from well-known testing libraries to pytest-httpx.

From responses

Feature responses pytest-httpx
Add a response responses.add() httpx_mock.add_response()
Add a callback responses.add_callback() httpx_mock.add_callback()
Retrieve requests responses.calls httpx_mock.get_requests()

Add a response or a callback

Undocumented parameters means that they are unchanged between responses and pytest-httpx. Below is a list of parameters that will require a change in your code.

Parameter responses pytest-httpx
method method=responses.GET method="GET"
body (as bytes) body=b"sample" content=b"sample"
body (as str) body="sample" text="sample"
status code status=201 status_code=201
headers adding_headers={"name": "value"} headers={"name": "value"}
content-type header content_type="application/custom" headers={"content-type": "application/custom"}
Match the full query match_querystring=True The full query is always matched when providing the url parameter.

Sample adding a response with responses:

from responses import RequestsMock

def test_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 with pytest-httpx:

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,
    )

From aioresponses

Feature aioresponses pytest-httpx
Add a response aioresponses.method() httpx_mock.add_response(method="METHOD")
Add a callback aioresponses.method() httpx_mock.add_callback(method="METHOD")

Add a response or a callback

Undocumented parameters means that they are unchanged between responses and pytest-httpx. Below is a list of parameters that will require a change in your code.

Parameter responses pytest-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 code status=201 status_code=201

Sample adding a response with aioresponses:

import pytest
from aioresponses import aioresponses


@pytest.fixture
def mock_aioresponse():
    with aioresponses() as m:
        yield m


def test_response(mock_aioresponse):
    mock_aioresponse.get(
        url="https://test_url",
        body=b"This is the response content",
        status=400,
    )

Sample adding the same response with pytest-httpx:

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,
    )

pytest_httpx's People

Contributors

apakottur avatar calebho avatar colin-b avatar dolfandringa avatar felixscherz avatar jakul avatar johnnydeuss avatar k900 avatar kostyaten avatar mezuzza avatar mgorny avatar mvbrn avatar rouge8 avatar shirblc avatar thomasleveil avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

pytest_httpx's Issues

Matching the `Authorization` header correctly while another parameter doesn't match causes a very confusing error message

With the following test, which correctly matches the Authorization header, but with an incorrect url in this case, the non-matching request causes a very confusing error message that seems to suggest the headers are wrong (besides the url also being wrong):

from pytest_httpx import HTTPXMock
import httpx


def test_something(httpx_mock: HTTPXMock):
    httpx_mock.add_response(
        method="GET",
        url="http://www.google.com?q=b",
        match_headers={"Authorization": "Bearer: Something"},
    )
    with httpx.Client() as client:
        client.get(
            "http://www.google.com", headers={"Authorization": "Bearer: Something"}
        )

Running this with pytest results in the following error:

E       httpx.TimeoutException: No response can be found for GET request on http://www.google.com with {} headers amongst:
E       Match GET requests on http://www.google.com?q=b with {'Authorization': 'Bearer: Something'} headers

I was expecting it to fail, but it says with {} header as if no headers were passed in, while they were.

I found out what the issue is:
In _explain_that_no_response_was_found it includes if name in expect_headers}. The issue is that in the request, the Authorization header is turned into lowercase. So this ends up on that line as "authorization" not in {"Authorization": "Bearer: Something"}, so it won't display the header.

release to support httpx `0.20.x`

Hi all, Thanks for the great library; httpx released 0.20 yesterday I believe, if pytest-httpx could support that it would be great for those of us using both of these libraries; Happy to take a look myself - would it also be possible to have the hacktoberfest topic added to the repository? :)

Thanks again.

edit: Tests are all failing for me locally even on develop

Dynamic callback mock is not called when there were matching already used static responses.

Adding a callback mock for an URL which matched previously to any static response mock has no effect.
Seems that pytext-httpx chooses to use last (even already used) static response, even though there is an unused callback mock waiting to be picked.
This is counter-intuitive and incompatible at least with aioresponses.

@pytest.mark.asyncio
async def test_callback_order(httpx_mock: HTTPXMock):
    async with httpx.AsyncClient() as client:
        httpx_mock.add_response(url='http://localhost/api', json={'response': 'first'})
        assert 'first' == (await client.get('http://localhost/api')).json()['response']

        httpx_mock.add_response(url='http://localhost/api', json={'response': 'second'})
        assert 'second' == (await client.get('http://localhost/api')).json()['response']

        def _callback(req: httpx.Request):
            return httpx.Response(status_code=200, json={'response': 'third'})

        httpx_mock.add_callback(_callback, url='http://localhost/api')
        assert 'third' == (await client.get('http://localhost/api')).json()['response']

The result:

>           assert 'third' == (await client.get('http://localhost/api')).json()['response']
E           AssertionError: assert 'third' == 'second'
E             - second
E             + third

mocked but not requested

Hi! I'm trying your pytest fixture, unfortunately I get errors like this:

 AssertionError: The following responses are mocked but not requested: [(b'HTTP/1.1', 200, b'', [], <httpx._content_streams.ByteStream object at 0x7f05af8d0160>), (b'HTTP/1.1', 200, b'', [], <httpx._content_streams.ByteStream object at 0x7f05af8d0358>)]

My tests look like this:

import pytest
from pytest_httpx import httpx_mock

from my_package import my_module

@pytest.mark.asyncio
async def test_get_user_from_sso_with_empty_cookie(httpx_mock):
    httpx_mock.add_response()
    with pytest.raises(ConnectionError):
        await my_module.my_function(cookie="")


@pytest.mark.asyncio
async def test_get_user_from_sso_with_missing_cookie(httpx_mock):
    httpx_mock.add_response()
    with pytest.raises(ConnectionError):
        await my_module.my_function(cookie=None)

my_module.my_function is then using HTTPX async clients to send requests to another service.

Any idea why this is happening?

I'm using HTTPX v0.13.1 and pytest_httpx v0.3.0

How can I send multiple responses matching the same request?

Hello,

thanks a lot for this project 🙏

The only thing I miss until now, is an option to define a response list, like it exists for requests-mock.

I need it to simulate a retry. Unlike in #75 my requests are identical, so unfortunately I have no header or something like that, which I can use to distinguish the requests.

fin swimmer

If the url query parameter contains Chinese characters, it will cause an encoding error

httpx_mock.add_response(
      url='test_url?query_type=数据',
      method='GET',
      json={'result': 'ok'}
 )

Executing the above code, It wil cause an encoding error:

obj = '数据', encoding = 'ascii', errors = 'strict'

def _encode_result(obj, encoding=_implicit_encoding,
                        errors=_implicit_errors):
   return obj.encode(encoding, errors)

E UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

/usr/local/Cellar/[email protected]/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/urllib/parse.py:108: UnicodeEncodeError

Question: How to mock only certain requests?

Hi there,
Quick question. I'm using httpx for testing a webserver AND making external requests inside the webserver too.

I want requests to my webserver to go untouched. But I want to mock out all sub-requests made by my server.

Example:
Normal: http://testserver
Mocked: https://wikipedia...

I could inject, but I'm wondering if there is some functionality to disable mocking for certain urls. It seems like httpx_mock mocks everything.

Thanks in advance.
Phil

`Response.elapsed` not set

Problem
Response.elapsed isn't set after a mocked POST if add_response is given json=... or content=b....

Reproduce
This snippet replicates the problem for me:

def test_elapsed(httpx_mock: HTTPXMock):
    httpx_mock.add_response(json={})
    r = httpx.post(
        "https://jsonplaceholder.typicode.com/posts",
        data={"title": "foo", "body": "bar", "userId": 1,},
        headers={"Content-type": "application/json; charset=UTF-8"},
    )
    r.raise_for_status()
    r.close()
    print(r.elapsed.total_seconds())

Versions
httpx=0.21.3
pytest_httpx=0.15.0

Add a way to reset HTTPXMock._requests

I started using httpx_mock and tried to mock multiple requests to one resource.
Then I expected httpx_mock.reset() to reset the state of HTTPXMock. However, it resets only callbacks, self._requests are still left unchanged.

Generally:

def test_something(httpx_mock: HTTPXMock):
    # custom setup that has to be here, but includes mocked requests
    httpx_mock.reset() 
    for i in range(5):
        httpx.request(...)
    assert len(httpx_mock.get_requests()) == 5 # fails, because of the mocked requests in "setup" part. There is no way to reset list of requests

Is there a reason why .reset() is not resetting requests list?

Allow to record and store requests+responses

First iteration on this feature should allow to discard a specified set of headers from the requests (defaulting to whatever might change in between runs, such as the agent)
First iteration will only allow storage as a file provided by a path.

Update to httpx 0.23.0

httpx released version 0.23.0 this morning, which corrects a significant security issue. It also removes support for Python 3.6.

I'd like to update my projects to use the new version alongside pytest_httpx. Could the project update its dependency, or would you welcome a PR to make that change?

httpx.Response.elapsed cannot be accessed anymore

I have logging that reads request time from .elapsed attribute of httpx.Request. Everything works with 0.13, but after upgrading to 0.15 I'm getting exceptions:

'.elapsed' may only be accessed after the response has been read or closed.

My versions:
httpx==0.21.1
pytest-httpx==0.15.0

Document how to assert that no responses are sent

Similar to #18, but it wasn't resolved there. Suppose we have

def test_no_requests(httpx_mock):
    httpx_mock.add_response()
    my_function(...)
    assert not len(httpx_mock.get_requests())

This will throw at teardown unless you override the assert_all_responses_were_requested fixture to return False. However, this behavior might be desirable for other tests in the file. From what I can tell, there's no way to define the return value of the foregoing fixture depending on the test.

Maybe there should be an API on HTTPXMock which disables this check so that we have:

def test_no_requests(httpx_mock):
    httpx_mock.assert_all_responses_were_requested = False  # default is True
    httpx_mock.add_response()
    my_function(...)
    assert not len(httpx_mock.get_requests())

Logging Error: "TypeError: %d format: a real number is required, not re.Pattern"

I recently encountered this error in our tests, however it happens only occasionally and I haven't been able to reliably reproduce it. At first glance it seems like the status_code of the Response is somehow getting set to a re.Pattern object.

Hopefully someone more knowledge of pytest_httpx may recognize the problem; however in the meantime I'll continue to investigate and see if I can come up with a minimal reproducible example.

Here is the line that is (presumably) causing the error:

httpx_mock.add_response(re.compile(r"http://www.example.com.*"), text="wat")

Here is the full pytest traceback:

httpx_mock = <pytest_httpx._httpx_mock.HTTPXMock object at 0x7fd13a696710>, helpers = <bbot.core.helpers.helper.ConfigAwareHelper object at 0x7fd13161b340>

    @pytest.mark.asyncio
    async def test_web_http_compare(httpx_mock, helpers):
        httpx_mock.add_response(re.compile(r"http://www.example.com.*"), text="wat")
        compare_helper = helpers.http_compare("http://www.example.com")
>       await compare_helper.compare("http://www.example.com", headers={"asdf": "asdf"})

bbot/test/test_step_2/test_web.py:155: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
bbot/core/helpers/diff.py:137: in compare
    await self._baseline()
bbot/core/helpers/diff.py:28: in _baseline
    baseline_1 = await self.parent_helper.request(
bbot/core/helpers/web.py:99: in request
    response = await client.request(*args, **kwargs)
../../../.cache/pypoetry/virtualenvs/bbot-yxGMlPK5-py3.10/lib/python3.10/site-packages/httpx/_client.py:1530: in request
    return await self.send(request, auth=auth, follow_redirects=follow_redirects)
../../../.cache/pypoetry/virtualenvs/bbot-yxGMlPK5-py3.10/lib/python3.10/site-packages/httpx/_client.py:1617: in send
    response = await self._send_handling_auth(
../../../.cache/pypoetry/virtualenvs/bbot-yxGMlPK5-py3.10/lib/python3.10/site-packages/httpx/_client.py:1645: in _send_handling_auth
    response = await self._send_handling_redirects(
../../../.cache/pypoetry/virtualenvs/bbot-yxGMlPK5-py3.10/lib/python3.10/site-packages/httpx/_client.py:1682: in _send_handling_redirects
    response = await self._send_single_request(request)
../../../.cache/pypoetry/virtualenvs/bbot-yxGMlPK5-py3.10/lib/python3.10/site-packages/httpx/_client.py:1729: in _send_single_request
    logger.info(
/usr/lib/python3.10/logging/__init__.py:1477: in info
    self._log(INFO, msg, args, **kwargs)
/usr/lib/python3.10/logging/__init__.py:1624: in _log
    self.handle(record)
/usr/lib/python3.10/logging/__init__.py:1634: in handle
    self.callHandlers(record)
/usr/lib/python3.10/logging/__init__.py:1696: in callHandlers
    hdlr.handle(record)
/usr/lib/python3.10/logging/__init__.py:968: in handle
    self.emit(record)
../../../.cache/pypoetry/virtualenvs/bbot-yxGMlPK5-py3.10/lib/python3.10/site-packages/_pytest/logging.py:350: in emit
    super().emit(record)
/usr/lib/python3.10/logging/__init__.py:1108: in emit
    self.handleError(record)
/usr/lib/python3.10/logging/__init__.py:1100: in emit
    msg = self.format(record)
/usr/lib/python3.10/logging/__init__.py:943: in format
    return fmt.format(record)
../../../.cache/pypoetry/virtualenvs/bbot-yxGMlPK5-py3.10/lib/python3.10/site-packages/_pytest/logging.py:114: in format
    return super().format(record)
/usr/lib/python3.10/logging/__init__.py:678: in format
    record.message = record.getMessage()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <LogRecord: httpx, 20, /home/bls/.cache/pypoetry/virtualenvs/bbot-yxGMlPK5-py3.10/lib/python3.10/site-packages/httpx/_client.py, 1729, "HTTP Request: %s %s "%s %d %s"">

    def getMessage(self):
        """
        Return the message for this LogRecord.
    
        Return the message for this LogRecord after merging any user-supplied
        arguments with the message.
        """
        msg = str(self.msg)
        if self.args:
>           msg = msg % self.args
E           TypeError: %d format: a real number is required, not re.Pattern

/usr/lib/python3.10/logging/__init__.py:368: TypeError

pyright with strict typeCheckingMode reports error: Type of "add_response" is partially unknown because of **matchers

with pytest_httpx_bug_demo.py

from pytest_httpx import HTTPXMock
import httpx


def test_something(httpx_mock: HTTPXMock):
    httpx_mock.add_response(method="GET", url="http://www.google.com")
    with httpx.Client() as client:
        client.get("http://www.google.com")

running pyright in stict mode with the following pyproject.toml

[tool.pyright]
typeCheckingMode = "strict"

results in the following error

pyright pytest_httpx_bug_demo.py
/home/[redacted]/pytest_httpx_bug_demo.py
  /home/[redacted]/pytest_httpx_bug_demo.py:6:5 - error: Type of "add_response" is partially unknown
    Type of "add_response" is "(status_code: int = 200, http_version: str = "HTTP/1.1", headers: Headers | Dict[str, str] | Dict[bytes, bytes] | Sequence[Tuple[str, str]] | Sequence[Tuple[bytes, bytes]] = None, content: bytes | None = None, text: str | None = None, html: str | None = None, stream: Any = None, json: Any = None, **matchers: Unknown) -> None" (reportUnknownMemberType)
1 error, 0 warnings, 0 informations

This is because the **marchers arguments don't have any type declaration. Changing that into **matchers: Any would addresses that.

suggestions for add assert_all_requests_are_fired

good job!👍🏻
I realy like the project and it dost help me a lot.
in some test I use pytest_httpx to mock some requests but I do not need to ensure all requests will be sent
so , perhaps it's a good feature to add a toggle assert_all_requests_are_fired just like what responses do?

How do simulate multiple responses, similar to `side_effect` in mock?

I have a method where if a call received a 401 response, it then calls the auth API to refresh its auth token, and then re-issue the call with the new token. When done synchronously with requests, I can mock out the call with multiple responses in the side_effect parameter.

Moving to httpx and async calls, I'm stumped how to test this case. I tried adding multiple httpx_mock.add_reponse() calls, but of course that didn't work. Any advice?

Problem running pytests in GitHub environment

I have a FastAPI application that's using httpx, and I'm having trouble getting the pytests to run successfully in the GitHub environment when I push the code. The tests run fine when run in a local Docker container.

Here's my Dockerfile

FROM python:3.9.7-slim
WORKDIR /app/
ENV PYTHONPATH "${PYTHONPATH}:/"
COPY requirements.txt .
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0"]

And here's the requirements.txt file it uses to get the modules the application needs:

fastapi==0.82.0
uvicorn==0.18.3
pydantic==1.10.2
pylint==2.15.2
pytest==7.1.3
boto3==1.24.68
python-dotenv==0.21.0
pytest-mock==3.8.2
sseclient==0.0.27
pytest-asyncio==0.19.0
wheel==0.37.1
httpx==0.23.0
pytest-httpx==0.21.0
pytest-trio==0.7.0

Here's the .github/workflows/unit-tests.yml file that runs the tests in the GitHub environment:

name: unit-tests

on: [push]
jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 1
          ref: ${{ github.event.inputs.branch_name }}

      - name: Set up Python 3.9.x
        uses: actions/setup-python@v1
        with:
          python-version: 3.9.x

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install fastapi==0.82.0
          pip install pydantic==1.10.2
          pip install pytest==7.1.3
          pip install boto3==1.24.68
          pip install python-dotenv==0.21.0
          pip install pytest-mock==3.8.2
          pip install sseclient==0.0.27
          pip install pytest-asyncio==0.19.0
          pip install httpx==0.23.0
          pip install pytest-httpx==0.21.0
          pip install pytest-cov==4.0.0
          pip install pytest-trio==0.7.0

      - name: Running unit tests
        run: |
          pytest -v --cov=app tests/unit/ --asyncio-mode=strict

And here's part of the errors generated when run in the GitHub environment:

Run pytest --cov=app tests/unit/ --asyncio-mode=strict
============================= test session starts ==============================
platform linux -- Python 3.9.14, pytest-7.1.3, pluggy-1.0.0
rootdir: /home/runner/work/integration-engine/integration-engine
plugins: httpx-0.21.0, anyio-3.[6](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:7).1, cov-4.0.0, asyncio-0.19.0, mock-3.8.2, trio-0.[7](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:8).0
asyncio: mode=strict
collected 69 items

tests/unit/test_main.py .                                                [  1%]
tests/unit/dependencies/test_validate_requests.py ..                     [  4%]
tests/unit/helpers/test_agent_command_helper.py .......                  [ 14%]
tests/unit/helpers/test_agent_message_helper.py ....                     [ 20%]
tests/unit/helpers/test_cognito_helper.py ssFF                           [ 26%]
tests/unit/helpers/test_mercure_helper.py .....                          [ 33%]
tests/unit/helpers/test_pmb_helper.py ....                               [ 39%]
tests/unit/helpers/test_repository_helper.py .......                     [ 49%]
tests/unit/repositories/test_agent_message_repository.py ...             [ 53%]
tests/unit/repositories/test_agent_repository.py .....                   [ 60%]
tests/unit/routers/test_agent_communication.py ..............            [ [8](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:9)1%]
tests/unit/routers/test_agent_search.py ....                             [ 86%]
tests/unit/routers/test_authentication.py ..                             [ 8[9](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:10)%]
tests/unit/routers/test_diagnostics.py ..                                [ 92%]
tests/unit/routers/test_healthz.py ..                                    [ 95%]
tests/unit/routers/test_registration.py ...                              [[10](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:11)0%]

=================================== FAILURES ===================================
____ test_get_token_with_invalid_credentials_returns_error_message[asyncio] ____

httpx_mock = <pytest_httpx._httpx_mock.HTTPXMock object at 0x7f9346b296d0>

    @pytest.mark.anyio
    async def test_get_token_with_invalid_credentials_returns_error_message(httpx_mock: HTTPXMock):
        """Test that get token with invalid credentials returns error message"""
        mock_valid_response = {
            "errorMessage": "test_error_message"
        }
        mock_user_auth_request = UserAuthenticationRequest(
            username="test_invalid_username",
            ***
        )
        httpx_mock.add_response(json=mock_valid_response)
>       response = await get_token(mock_user_auth_request)

tests/unit/helpers/test_cognito_helper.py:50: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
app/helpers/cognito_helper.py:37: in get_token
    data = await call_cognito(url=url, body=body)
app/helpers/cognito_helper.py:59: in call_cognito
    request = httpx.Request("GET", url=url, json=body)
/opt/hostedtoolcache/Python/3.9.[14](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:15)/x64/lib/python3.9/site-packages/httpx/_models.py:326: in __init__
    self.url = URL(url)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <[AttributeError("'URL' object has no attribute '_uri_reference'") raised in repr()] URL object at 0x7f9346aa[23](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:24)70>
url = None, kwargs = {}

    def __init__(
        self, url: typing.Union["URL", str, RawURL] = "", **kwargs: typing.Any
    ) -> None:
        if isinstance(url, (str, tuple)):
            if isinstance(url, tuple):
                raw_scheme, raw_host, port, raw_path = url
                scheme = raw_scheme.decode("ascii")
                host = raw_host.decode("ascii")
                if host and ":" in host and host[0] != "[":
                    # it's an IPv6 address, so it should be enclosed in "[" and "]"
                    # ref: https://tools.ietf.org/html/rfc[27](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:28)[32](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:33)#section-2
                    # ref: https://tools.ietf.org/html/rfc[39](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:40)86#section-3.2.2
                    host = f"[{host}]"
                port_str = "" if port is None else f":{port}"
                path = raw_path.decode("ascii")
                url = f"{scheme}://{host}{port_str}{path}"
    
            try:
                self._uri_reference = rfc3986.iri_reference(url).encode()
            except rfc3986.exceptions.InvalidAuthority as exc:
                raise InvalidURL(message=str(exc)) from None
    
            if self.is_absolute_url:
                # We don't want to normalize relative URLs, since doing so
                # removes any leading `../` portion.
                self._uri_reference = self._uri_reference.normalize()
        elif isinstance(url, URL):
            self._uri_reference = url._uri_reference
        else:
>           raise TypeError(
                f"Invalid type for url.  Expected str or httpx.URL, got {type(url)}: {url!r}"
            )
E           TypeError: Invalid type for url.  Expected str or httpx.URL, got <class 'NoneType'>: None

/opt/hostedtoolcache/Python/3.9.14/x[64](https://github.com/rectanglehealth/integration-engine/actions/runs/3207285033/jobs/5242003697#step:5:65)/lib/python3.9/site-packages/httpx/_urls.py:102: TypeError

My apologies if this isn't the right forum to be asking this, but I'm at my wits end trying to resolve this. Any ideas, pointers or suggestions are welcome.

Allow to add responses based on a file content

This will allow to replay #113 and have the side effect of letting users have responses in files instead of in python tests (especially useful in case of heavy amount of responses or even huge payloads).
Argument parsing should be intelligent enough to allow every existing parameter to date (and be generic in some way?)

locked on pytest 6

Hi,

Thanks for the nice library.
Currently, pytest-httpx depends on pytest ==6.*

But there is a fix to an annoying async issue only available in pytest 7.2+
pytest-dev/pytest#3747

Could you please increment the dependency version?
Thank you

Consider adding more request matchers

Hi !!

It is possible to match request attributes? These attributes are timeout, auth, and others.
I'm trying to use this to ensure that the request has these attributes.

At the documentation has headers, methods, and content. Can we develop new feature?

add_callback is no longer valid method to raise exception.

Since version 0.16.0 method add_callback has been requiring callback with httpx.Response return value. If we create callback to only rise exception then return value is None. mypy won't pass with such expected return (I had to ignore triggering add_callback). I know that #10 would probably fix it but maybe You'll find some way to patch it sooner. I could write -> httpx.Response but it just doesn't seem True if all I do in function is raising an exception.

Assert response not called/fired?

Some functions I'm testing are caching results. I'd like to assert that subsequent calls do not hit the network.

I'm thinking of something like this:

def test_cached_results(httpx_mock):
    response = httpx_mock.add_response()
    my_function(...)
    
    # in two statements:
    assert not response.fired
    httpx_mock.remove(response)

    # in one statement: pop and assert not fired
    assert not httpx_mock.pop().fired

    # one statement, variant: if the response exists, it is removed, otherwise it fails
    assert httpx_mock.remove(response)

Registration order matters when multiple responses match a request, can regex be a specific case?

Take this example:

httpx_mock.add_response(
    url=re.compile(r"http://example.com/"),
    status_code=404,
)

httpx_mock.add_response(
    url="http://example.com/test/123",
    status_code=200,
)

In this case, my expected behavior is:

First, match the more specific URL, otherwise fallback to more general URL.

However, the code above ends up creating flaky tests; most of the time, it works, but sometimes it fails when it matches the regex first and not the specific case.

As far as I understand, this can be solved by defining specific matchers before the general ones. And that's fine.

I have two suggestions:

  1. Document this behavior and keep it as is.
  2. Raise some exception when two different matchers match the same thing.

Same response cannot be read more than once

add_response()
stream first request
stream second request // failure because stream is already consumed by first read of the same response, even if this is the same request

Don't use single Response instance for all requests.

import httpx
import pytest
import pytest_httpx


@pytest.mark.asyncio
async def test_something(httpx_mock):
    httpx_mock.add_response(json={"abc": "def"})
    with httpx.Client() as client:
        # response.stream will be a BoundSyncStream
        client.get("https://example.com/")
        # response.stream will be a BoundSyncStream, referencing a BoundSyncStream
        client.get("https://example.com/")
        # response.stream will be a BoundSyncStream, referencing a BoundSyncStream, referencing a BoundSyncStream
        client.get("https://example.com/")

The issue is essentially that a single response instance is being returned by the httpx_mock transport, and being reused.

Originally posted by @tomchristie in encode/httpx#2777 (comment)

Package for conda-forge

I love this package but was wondering if we could also have a conda version of it on conda-forge as I work almost exclusively in conda environments and even though I can still pip install it there upgrades would work more smoothly if pytest_httpx was available as a conda package too.

The process to create - and maintain - a package on conda-forge is relatively easy and straight-forward for pure Python pip packages. Happy to help with this if you're open to the idea. From a quick check of the dependencies these all appear to available on conda-forge too so I don't anticipate any problems there.

Unable to match on JSON content

When mocking a request body with match_content, the value passed in gets converted to bytes. However, when making the actual request, the body gets converted to json, even if I use content and pass an array of bytes. Therefore, I'm unable to match any requests when using the match_content parameter.

I would like to be able to send json in the request and set a matcher on the mocker to only pass when a request with that json body is sent.

Request:

async with AsyncClient() as client:
    response = await client.post("api.test.com", json={"userId": 1, "data": {"names": ["Test1", "Test2"]}})

Test:

@pytest.mark.asyncio
async def test_send_request(httpx_mock: HTTPXMock):
    httpx_mock.add_response(url="api.test.com", method="POST", match_content={"userId": 1, "data": {"names": ["Test1", "Test2"]}})
    await send_request()

Test failure message:

httpx.TimeoutException: No response can be found for POST request on /api.test.com with b'{"userId": 1, "data": {"names": ["Test1", "Test2"]}}' body amongst:
Match POST requests on api.test.com with {'userId': 1, 'data': {'names': ['Test1', 'Test2']}} body

Async callbacks feature request

I have a potential use case for an async callback which was originally mentioned in #38. I have the following (pseudo-)code which I want to test:

async def foo():
    async with httpx.AsyncClient() as c:
        with trio.move_on_after(timeout) as cs:
            await c.post(endpoint, json=payload)
        if cs.cancelled_caught:
            ...  # do stuff
        else:
            ...  # do other stuff

This is the test function I want to write

async def test_foo_timeout(httpx_mock, autojump_clock):
    async def cb(*args):
        await trio.sleep(some_long_time)
    
    httpx_mock.add_callback(cb)

    foo()

    ...  # assertions about what happens if the request times out

Importantly, autojump_clock virtualizes the clock so that my test executes immediately. It requires use of trio.sleep instead of other sleep functions, e.g. time.sleep.

Alternatively, I could inject a post function to foo and avoid mocking altogether, i.e.

async def foo(post):
    with trio.move_on_after(timeout) as cs:
        await post(endpoint, payload)
    if cs.cancelled_caught:
        ...  # do stuff
    else:
        ...  # do other stuff

In which case my test would become

async def test_foo_timeout(autojump_clock):
    async def post(*args):
        await trio.sleep(some_long_time)

    foo(post)

    ...  # assertions about what happens if the request times out

Let me know if this makes sense or there is another alternative which I didn't consider.

httpx does not read the response if an event_hook raises an exception

This code is based on https://github.com/Colin-b/pytest_httpx#reply-with-custom-body :

def test_str_body(httpx_mock: HTTPXMock):
    httpx_mock.add_response(text="This is my UTF-8 content", status_code=httpx.codes.BAD_REQUEST)

    with httpx.Client(
        event_hooks={
            "response": [lambda response: response.raise_for_status()]
        }
    ) as client:
        try:
            client.get("https://test_url")
        except httpx.HTTPStatusError as exc:
            assert exc.response.text == "This is my UTF-8 content"

It fails with this error:

httpx.ResponseNotRead: Attempted to access streaming response content, without having called `read()`.

Conda cannot install pytest 7 and pytest-httpx 0.20.0

environment.yml

name: api
channels:
  - conda-forge
dependencies:
  - python==3.9
  - fastapi>=0.73.0
  - uvicorn[standard]>=0.17.4
  - SQLAlchemy>=1.4.31
  - psycopg2>=2.9.3
  - httpx>=0.22.0
  - alembic>=1.7.6
  - SQLAlchemy-Utils>=0.38.2
  - pre-commit>=2.9.3
  - pytest>=7.0.1
  - requests>=2.27.1
  - pytest-httpx>=0.20.0 # conflicts begin when adding this package.
❯ conda env update
Collecting package metadata (repodata.json): done
Solving environment: | 
Found conflicts! Looking for incompatible packages.
This can take several minutes.  Press CTRL-C to abort.
failed                                                                                                                                                                        

UnsatisfiableError: The following specifications were found to be incompatible with a past
explicit spec that is not an explicit spec in this operation (sqlalchemy):

  - alembic[version='>=1.7.6'] -> sqlalchemy[version='>=1.3.0']
  - httpx[version='>=0.22.0']
  - psycopg2[version='>=2.9.3']
  - pytest-httpx[version='>=0.20.0'] -> httpx=0.22
  - pytest-httpx[version='>=0.20.0'] -> pytest=6
  - pytest[version='>=7.0.1']
  - requests[version='>=2.27.1']
  - sqlalchemy-utils[version='>=0.38.2'] -> sqlalchemy[version='>=1.0']
  - sqlalchemy[version='>=1.4.31']

From this CHANGELOG entry it appears as though pytest 7 should be supported.

I'm pretty new to python package management, so I'm not sure if I'm doing something dumb. Also since we're using conda-forge, I checked that artifact repository and I see 0.20 was uploaded 19 days ago, so I'm assuming they have the most up-to-date version.

If I switch from
pytest>=7.0.1 => pytest>=6.2.5

❯ conda env update
Collecting package metadata (repodata.json): done
Solving environment: done

Downloading and Extracting Packages
pytest-httpx-0.20.0  | 15 KB     | ################################################################################################################################### | 100% 
pytest-6.2.5         | 434 KB    | ################################################################################################################################### | 100%
Preparing transaction: done
Verifying transaction: done
Executing transaction: done

Any pointers would be appreciated.

Thanks!

Add a create_response function

That we will use and will offer to clients the ability to create responses without having to rely on httpx internals themselves

Automatic snapshot of Requests

Hi, I have a use-case where I'm querying over 80 different endpoints.
Currently the outputs are verified using pytest-snapshot.
But the tests won't run fast as the HTTP calls have to be made.

What I'm trying to achieve is a system where if I add a new module and run the test, it's HTTP calls will be run once and written to a file. For future runs, that response will be provided by the mock, so we can skip the HTTP calls.

Is such an approach feasible with enough modifications in pytest_httpx, or are there any architectural limitations which would stop me from adding such a feature?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.