283 words
1 minute
How to mock httpx using pytest-mock

How to mock httpx using pytest-mock#

I wrote this test to exercise some httpx code today, using pytest-mock.

The key was to use mocker.patch.object(cli, "httpx") which patches the httpx module that was imported by the cli module.

Here the mocker function argument is a fixture that is provided by pytest-mock.

from conditional_get import cli
from click.testing import CliRunner
def test_performs_conditional_get(mocker):
m = mocker.patch.object(cli, "httpx")
m.get.return_value = mocker.Mock()
m.get.return_value.status_code = 200
m.get.return_value.content = b"Hello PNG"
m.get.return_value.headers = {"etag": "hello-etag"}
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(
cli.cli, ["https://example.com/file.png", "-o", "file.png"]
)
m.get.assert_called_once_with("https://example.com/file.png", headers={})
assert b"Hello PNG" == open("file.png", "rb").read()
# Should have also written the ETags file
assert {"https://example.com/file.png": "hello-etag"} == json.load(
open("etags.json")
)
# Second call should react differently
m.get.reset_mock()
m.get.return_value.status_code = 304
result = runner.invoke(
cli.cli, ["https://example.com/file.png", "-o", "file.png"]
)
m.get.assert_called_once_with(
"https://example.com/file.png", headers={"If-None-Match": "hello-etag"}
)

https://github.com/simonw/conditional-get/blob/485fab46f01edd99818b829e99765ed9ce0978b5/tests/test_cli.py

Mocking a JSON response#

Here’s a mock for a GraphQL POST request that returns JSON:

@pytest.fixture
def mock_graphql_region(mocker):
m = mocker.patch("datasette_publish_fly.httpx")
m.post.return_value = mocker.Mock()
m.post.return_value.status_code = 200
m.post.return_value.json.return_value = {"data": {"nearestRegion": {"code": "sjc"}}}

https://github.com/simonw/datasette-publish-fly/blob/5253220bded001e94561e215d553f352838e7a1c/tests/test_publish_fly.py#L16-L21

Mocking httpx.stream#

I later had to figure out how to mock the following:

with httpx.stream("GET", url, headers=headers) as response:
...
with open(output, "wb") as fp:
for b in response.iter_bytes():
fp.write(b)

https://stackoverflow.com/a/6112456 helped me figure out the following:

def test_performs_conditional_get(mocker):
m = mocker.patch.object(cli, "httpx")
m.stream.return_value.__enter__.return_value = mocker.Mock()
m.stream.return_value.__enter__.return_value.status_code = 200
m.stream.return_value.__enter__.return_value.iter_bytes.return_value = [
b"Hello PNG"
]

https://github.com/simonw/conditional-get/blob/80454f972d39e2b418572d7938146830fab98fa6/tests/test_cli.py

Mocking an HTTP error triggered by response.raise_for_status()#

The response.raise_for_status() raises an exception if an HTTP error (e.g. a 404 or 500) occurred.

Here’s how I mocked that to return an error:

def test_airtable_to_yaml_error(mocker):
m = mocker.patch.object(cli, "httpx")
m.get.return_value = mocker.Mock()
m.get.return_value.status_code = 401
m.get.return_value.raise_for_status.side_effect = httpx.HTTPError(
"Unauthorized", request=None
)
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(
cli.cli, [".", "appZOGvNJPXCQ205F", "tablename", "-v", "--key", "x"]
)
assert result.exit_code == 1
assert result.stdout == "Error: Unauthorized\n"
How to mock httpx using pytest-mock
https://mranv.pages.dev/posts/how-to-mock-httpx-using-pytest-mock/
Author
Anubhav Gain
Published at
2024-06-20
License
CC BY-NC-SA 4.0