til/pytest/mock-httpx.md
# How to mock httpx using pytest-mockI wrote this test to exercise some [httpx](https://pypi.org/project/httpx/) code today, using [pytest-mock](https://pypi.org/project/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`.```pythonfrom conditional_get import clifrom click.testing import CliRunnerdef test_performs_conditional_get(mocker):m = mocker.patch.object(cli, "httpx")m.get.return_value = mocker.Mock()m.get.return_value.status_code = 200m.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 fileassert {"https://example.com/file.png": "hello-etag"} == json.load(open("etags.json"))# Second call should react differentlym.get.reset_mock()m.get.return_value.status_code = 304result = 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 responseHere's a mock for a GraphQL POST request that returns JSON:```python@pytest.fixturedef mock_graphql_region(mocker):m = mocker.patch("datasette_publish_fly.httpx")m.post.return_value = mocker.Mock()m.post.return_value.status_code = 200m.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.streamI later had to figure out how to mock the following:```pythonwith 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:```pythondef 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 = 200m.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](https://github.com/simonw/airtable-to-yaml/blob/ebd94b2e29d6f2ec3dc64d161495a759330027e8/tests/test_airtable_to_yaml.py#L43-L56):```pythondef test_airtable_to_yaml_error(mocker):m = mocker.patch.object(cli, "httpx")m.get.return_value = mocker.Mock()m.get.return_value.status_code = 401m.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 == 1assert result.stdout == "Error: Unauthorized\n"```