# Mock and block real network calls in tests

> Real HTTP calls make pytest suites slow and flaky. Mock at the transport with responses, respx, pytest-httpx, aioresponses, moto or vcrpy, then block the network with pytest-socket.

A test that calls a real API waits for DNS, a TCP and TLS handshake, the remote server and the response, often hundreds of milliseconds per call. Retries and timeouts make the slow path much slower: a test that hits an unreachable host can hang for the client's whole timeout.

Network calls are also one of the top causes of [flaky tests](https://flakytest.dev/guides/common-causes-of-flaky-tests/). The test depends on a service you don't control, on rate limits and on the network of the machine running it, which is why these tests so often pass on a laptop and fail in CI (or the other way around).

## Why tests end up on the network

- The code under test calls an API, and nobody mocked it in this particular test.
- A mock was written for one HTTP library, and the code switched to another (`requests` to `httpx`).
- An SDK talks to the network on its own: fetching credentials or a region from instance metadata, sending telemetry, downloading a model or a schema.
- A fixture creates a real client against a staging environment "just for this test", and it spreads.

## Mock at the transport

Mock the HTTP layer rather than your own client functions. The code under test still builds the real request (URL, headers, body) and parses a real response, so you test your code, not your mock.

| Library | Mock with |
| --- | --- |
| requests | `responses`, or `pytest-httpserver` (a real local server) |
| httpx | `respx` or `pytest-httpx` |
| aiohttp | `aioresponses` |
| boto3 / botocore | `botocore.stub.Stubber`, or `moto` |
| anything | `vcrpy` / `pytest-recording` to record and replay real responses |

### requests: responses

```python
import responses

from myapp.weather import current_temperature


@responses.activate
def test_current_temperature():
    responses.get("https://api.example.com/weather", json={"temp": 21.5})

    assert current_temperature("Ljubljana") == 21.5
    assert responses.calls[0].request.params == {"city": "Ljubljana"}
```

Inside `@responses.activate`, a request to a URL you didn't register raises `ConnectionError` instead of going out.

### httpx: respx or pytest-httpx

```python
import httpx


def test_current_temperature(respx_mock):
    route = respx_mock.get("https://api.example.com/weather").mock(
        return_value=httpx.Response(200, json={"temp": 21.5})
    )

    assert current_temperature("Ljubljana") == 21.5
    assert route.called
```

With pytest-httpx the fixture is `httpx_mock`: `httpx_mock.add_response(url="https://api.example.com/weather?city=Ljubljana", json={"temp": 21.5})`. By default it fails the test at teardown if a registered response was never requested.

### aiohttp: aioresponses

```python
from aioresponses import aioresponses


async def test_current_temperature():
    with aioresponses() as mocked:
        mocked.get("https://api.example.com/weather?city=Ljubljana", payload={"temp": 21.5})
        assert await current_temperature("Ljubljana") == 21.5
```

### AWS: Stubber or moto

`botocore.stub.Stubber` queues exact responses for one client and checks the parameters. `moto` fakes whole services in memory, which suits tests that create a bucket, upload and read back:

```python
import boto3
from moto import mock_aws


@mock_aws
def test_upload_report():
    s3 = boto3.client("s3", region_name="eu-central-1")
    s3.create_bucket(Bucket="reports", CreateBucketConfiguration={"LocationConstraint": "eu-central-1"})

    upload_report(s3, "reports", b"data")

    assert s3.get_object(Bucket="reports", Key="report.csv")["Body"].read() == b"data"
```

### Record and replay: vcrpy and pytest-recording

For APIs with large or complicated responses, record them once and replay them from a cassette file. With pytest-recording, mark the test with `@pytest.mark.vcr`, run `pytest --record-mode=once` once to record, and commit the cassette. The default record mode is `none`, so later runs replay and never touch the network. Filter secrets out of cassettes with a `vcr_config` fixture (`{"filter_headers": ["authorization"]}`).

## Block accidental network access

Mocks only help in the tests that have them. To catch the rest, make any real network call fail the test.

### pytest-socket

```toml
[tool.pytest.ini_options]
addopts = "--disable-socket --allow-unix-socket"
```

`--disable-socket` makes any use of `socket` raise `SocketBlockedError`, DNS included. `--allow-unix-socket` keeps Unix domain sockets working (the asyncio event loop needs them, and so may a local database). If tests talk to a local database or Redis over TCP, use `--allow-hosts=127.0.0.1,::1` instead of `--disable-socket`: connections to any other host fail. Entries can be hostnames, IP addresses or CIDR ranges.

Re-enable the network per test with `@pytest.mark.enable_socket` or the `socket_enabled` fixture, or allow specific hosts with `@pytest.mark.allow_hosts(["127.0.0.1"])`.

pytest-recording has a similar `--block-network` option (with `--allowed-hosts`) that still lets tests marked `@pytest.mark.vcr` record.

### Without a plugin: patch getaddrinfo

Almost every client resolves the hostname with `socket.getaddrinfo` before connecting, so patching that one function catches requests from any HTTP library. This is the approach from [disabling network requests when running pytest](https://blog.pecar.me/disable-network-requets-when-running-pytest/):

```python
# conftest.py
import socket

import pytest


@pytest.fixture(autouse=True)
def block_external_requests(monkeypatch):
    original_getaddrinfo = socket.getaddrinfo
    allowed_hosts = {"localhost", "127.0.0.1", "::1", "0.0.0.0"}

    def assert_only_localhost(host, *args, **kwargs):
        assert host in allowed_hosts, f"External request to {host} detected"
        return original_getaddrinfo(host, *args, **kwargs)

    monkeypatch.setattr(socket, "getaddrinfo", assert_only_localhost)
```

The failure names the host, which tells you which mock is missing. It doesn't catch connections to a raw IP address (no lookup needed), which pytest-socket does.

## Keep the tests correct

- Keep a few explicitly marked integration tests that talk to the real service (`@pytest.mark.integration` plus `@pytest.mark.enable_socket`), and run them in a separate CI job, so a changed API doesn't go unnoticed.
- Mock responses drift from reality. Base them on real responses (recorded cassettes, or copies of real payloads) and refresh them when the API changes.
- Assert on the request too (URL, parameters, body), not only on what your code did with the response.

## How flakytest finds it

flakytest puts time spent in `socket`, `ssl`, `http.client`, `urllib3`, requests, httpx, aiohttp, botocore, gRPC, websockets, `smtplib` and DNS in the *network* category (database drivers and Redis clients have their own categories). When network time is more than 0.3 seconds and 1% of test time, it suggests **Tests make real network calls**, with the tests that spend the most time on the network and the lines of your code that make the calls. Compare a CI run with a local one on flakytest.dev to see whether the network is what makes CI slower.

## Further reading

- [responses](https://github.com/getsentry/responses)
- [respx](https://lundberg.github.io/respx/) and [pytest-httpx](https://colin-b.github.io/pytest_httpx/)
- [aioresponses](https://github.com/pnuckowski/aioresponses)
- [botocore Stubber](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/stubber.html) and [moto](https://docs.getmoto.org/)
- [pytest-recording](https://github.com/kiwicom/pytest-recording) and [vcrpy](https://vcrpy.readthedocs.io/)
- [pytest-socket](https://github.com/miketheman/pytest-socket)
