Newsletter
TechAnV Blog
Get updates on security engineering, Rust, eBPF, and DevSecOps. No spam, unsubscribe anytime.
Check your inbox and click the confirmation link to complete your subscription.
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.
1from conditional_get import cli2from click.testing import CliRunner3
4
5def test_performs_conditional_get(mocker):6 m = mocker.patch.object(cli, "httpx")7 m.get.return_value = mocker.Mock()8 m.get.return_value.status_code = 2009 m.get.return_value.content = b"Hello PNG"10 m.get.return_value.headers = {"etag": "hello-etag"}11 runner = CliRunner()12 with runner.isolated_filesystem():13 result = runner.invoke(14 cli.cli, ["https://example.com/file.png", "-o", "file.png"]15 )16 m.get.assert_called_once_with("https://example.com/file.png", headers={})17 assert b"Hello PNG" == open("file.png", "rb").read()18 # Should have also written the ETags file19 assert {"https://example.com/file.png": "hello-etag"} == json.load(20 open("etags.json")21 )22 # Second call should react differently23 m.get.reset_mock()24 m.get.return_value.status_code = 30425 result = runner.invoke(26 cli.cli, ["https://example.com/file.png", "-o", "file.png"]27 )28 m.get.assert_called_once_with(29 "https://example.com/file.png", headers={"If-None-Match": "hello-etag"}30 )Mocking a JSON response#
Here’s a mock for a GraphQL POST request that returns JSON:
1@pytest.fixture2def mock_graphql_region(mocker):3 m = mocker.patch("datasette_publish_fly.httpx")4 m.post.return_value = mocker.Mock()5 m.post.return_value.status_code = 2006 m.post.return_value.json.return_value = {"data": {"nearestRegion": {"code": "sjc"}}}Mocking httpx.stream#
I later had to figure out how to mock the following:
1with httpx.stream("GET", url, headers=headers) as response:2 ...3 with open(output, "wb") as fp:4 for b in response.iter_bytes():5 fp.write(b)https://stackoverflow.com/a/6112456 helped me figure out the following:
1def test_performs_conditional_get(mocker):2 m = mocker.patch.object(cli, "httpx")3 m.stream.return_value.__enter__.return_value = mocker.Mock()4 m.stream.return_value.__enter__.return_value.status_code = 2005 m.stream.return_value.__enter__.return_value.iter_bytes.return_value = [6 b"Hello PNG"7 ]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:
1def test_airtable_to_yaml_error(mocker):2 m = mocker.patch.object(cli, "httpx")3 m.get.return_value = mocker.Mock()4 m.get.return_value.status_code = 4015 m.get.return_value.raise_for_status.side_effect = httpx.HTTPError(6 "Unauthorized", request=None7 )8 runner = CliRunner()9 with runner.isolated_filesystem():10 result = runner.invoke(11 cli.cli, [".", "appZOGvNJPXCQ205F", "tablename", "-v", "--key", "x"]12 )13 assert result.exit_code == 114 assert result.stdout == "Error: Unauthorized\n"