Colin-b/pytest_httpx

Unable to match on JSON content

Closed this issue · 6 comments

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

Hello @colton-flyhomes

Thanks for raising this, for now the feature only works with bytes as there is no way to know for sure what you mean by a dict (is it a form content or a JSON content). So to fix your issue I would advise to match_content on the bytes representation of the JSON body.

I know it's far from ideal, especially when the order of the fields might change. This would be a nice improvement.

Hi @Colin-b

Thanks for the reply. I tried using bytes, however httpx converts the payload to json even if you pass in bytes using the content keyword.

You can use python dict as JSON content with httpx, it's not an issue. Just make sure that you provide bytes as the value for match_content parameter of the httpx_mock. Such as the following:

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

If you prefer to keep the JSON dict in your test case you can also use json.dumps followed by a encode() as in the following:

import json


def to_bytes(json_content: dict) -> bytes:
    return json.dumps(json_content).encode()


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

For me this is actually an issue. There is no way to guarantee the order of keys in a dictionary/json object. So match_content=json.dumps(my_test_data).encode('utf-8') won't work since python doesn't give any guarantee on the order of keys in a dictionary, so matching the order of the keys in my_test_data to the dictionary in the request before sending with httpx.post(json=mydata) is not possible.
A simple solution imo would be to add a match_json argument, and when present, it checks if the Content-Type header of the request is application/json and then it does a json.loads(request.body) and compares the result with match_json for equality.

Just to clarify this issue in case someone reads this in the future. The proposal is to check the body as JSON decoded content. The check on the Content-Type header will not be a part of this as there is more than one content-type value that can be used in conjonction with JSON encoded content and we wouldnt want to restrict our feature to only one type of usage.

Matching on headers can already be performed via match_headers.

Release 0.24.0 introducing the match_json parameter is now available on pypi.