Mocking Libraries and Small Tests¶
This guide explains how mocking libraries work with pytest-test-categories and why mocked tests remain hermetic even when they appear to make external calls.
Key Insight: Mocks Bypass the Network Blocker by Design¶
When you use mocking libraries like responses, respx, pytest-mock, or unittest.mock, your tests remain fully hermetic even though the code under test appears to make network calls. This is intentional and correct behavior.
Why? Mocking libraries intercept HTTP calls at the library level (requests, httpx, etc.) before they reach Python’s socket layer. Since our network blocker operates at the socket level, mocked calls never trigger it.
Without mocking:
Your code → requests.get() → socket.connect() → BLOCKED by pytest-test-categories
With mocking:
Your code → requests.get() → responses mock → returns fake data (never reaches socket)
Mocked Tests Are Still Hermetic¶
A test using mocks is hermetic because:
No actual network traffic occurs - The mock intercepts the call before any real connection
Tests are deterministic - Mock responses are controlled and predictable
Tests are fast - No network latency
Tests are isolated - No dependency on external services
Tests are parallelizable - No shared network resources
This is exactly the behavior you want for small tests.
Common Mocking Libraries¶
responses (for requests library)¶
The responses library mocks the requests library at the session level:
import pytest
import requests
import responses
@pytest.mark.small
@responses.activate
def test_api_client_fetches_user():
"""This test is hermetic - responses intercepts before socket layer."""
responses.add(
responses.GET,
"https://api.example.com/users/123",
json={"id": "123", "name": "Alice", "email": "alice@example.com"},
status=200,
)
# This call APPEARS to access the network but never does
response = requests.get("https://api.example.com/users/123")
assert response.status_code == 200
assert response.json()["name"] == "Alice"
respx (for httpx library)¶
The respx library mocks httpx at the transport level:
import pytest
import httpx
import respx
@pytest.mark.small
@respx.mock
def test_httpx_client():
"""Hermetic test using respx for httpx."""
respx.get("https://api.example.com/health").respond(
json={"status": "healthy"},
status_code=200,
)
response = httpx.get("https://api.example.com/health")
assert response.json()["status"] == "healthy"
pytest-mock (for any library)¶
The pytest-mock library provides the mocker fixture for general-purpose mocking:
import pytest
@pytest.mark.small
def test_user_service_creates_user(mocker):
"""Hermetic test using pytest-mock."""
# Mock the HTTP client at the point of use
mock_post = mocker.patch("myapp.services.httpx.post")
mock_post.return_value.json.return_value = {"id": "456", "name": "Bob"}
mock_post.return_value.status_code = 201
from myapp.services import UserService
service = UserService()
user = service.create_user(name="Bob", email="bob@example.com")
assert user["id"] == "456"
mock_post.assert_called_once()
unittest.mock (standard library)¶
Python’s built-in mocking works the same way:
import pytest
from unittest.mock import Mock, patch
@pytest.mark.small
@patch("requests.get")
def test_weather_service(mock_get):
"""Hermetic test using unittest.mock."""
mock_response = Mock()
mock_response.json.return_value = {"temp": 72, "conditions": "sunny"}
mock_response.status_code = 200
mock_get.return_value = mock_response
from myapp.weather import get_current_weather
weather = get_current_weather("Seattle")
assert weather["temp"] == 72
VCR.py (record/replay)¶
VCR.py records real HTTP interactions and replays them in subsequent test runs:
import pytest
import vcr
@pytest.mark.small
@vcr.use_cassette("tests/cassettes/user_api.yaml")
def test_user_api_response_format():
"""Hermetic test using recorded cassette."""
import requests
# First run: records real response to cassette file
# Subsequent runs: replays from cassette (no network access)
response = requests.get("https://api.example.com/users/123")
assert "id" in response.json()
assert "name" in response.json()
Note: After recording, VCR.py tests are hermetic. During the initial recording, you may need to temporarily use @pytest.mark.large or disable enforcement.
Architecture: How the Layers Work¶
Understanding the architecture helps explain why this works:
┌─────────────────────────────────────────────────────────────────┐
│ Your Test Code │
│ response = requests.get("https://api.example.com/users/1") │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Mocking Library Layer │
│ responses, respx, httpretty, pytest-mock, etc. │
│ │
│ ✓ Intercepts HTTP calls HERE │
│ ✓ Returns fake response │
│ ✗ Never calls lower layers │
└─────────────────────────────────────────────────────────────────┘
│
│ (only reached if NOT mocked)
▼
┌─────────────────────────────────────────────────────────────────┐
│ HTTP Library Layer │
│ requests, httpx, urllib3, aiohttp, etc. │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Socket Layer │
│ socket.socket.connect() │
│ │
│ ✗ pytest-test-categories blocks HERE │
│ ✗ Only reached if mocking is NOT active │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Operating System │
│ TCP/IP network stack │
└─────────────────────────────────────────────────────────────────┘
Best Practices¶
1. Choose the Right Mocking Library¶
Library |
Best For |
HTTP Library |
|---|---|---|
|
Simple request/response mocking |
|
|
Async-first, modern API |
|
|
Async HTTP mocking |
|
|
Library-agnostic mocking |
Any |
|
General-purpose, DI patterns |
Any |
|
Record/replay patterns |
Any |
2. Mock at the Right Level¶
Preferred: Mock at the HTTP library level (responses, respx)
@responses.activate
def test_user_fetch():
responses.add(responses.GET, "https://api.example.com/users/1", json={"id": 1})
# Test code that uses requests internally
Also good: Mock at your service boundary
def test_user_service(mocker):
mocker.patch.object(api_client, "get_user", return_value={"id": 1})
# Test code that uses api_client
Avoid: Mocking too deep (socket level) - let the blocker do its job
3. Keep Mocks Minimal¶
Only mock what you need to test:
# Good: Minimal mock for the test case
@pytest.mark.small
@responses.activate
def test_handles_404_error():
responses.add(responses.GET, "https://api.example.com/users/999", status=404)
with pytest.raises(UserNotFoundError):
fetch_user(999)
# Avoid: Over-mocking unrelated endpoints
@pytest.mark.small
@responses.activate
def test_handles_404_error():
responses.add(responses.GET, "https://api.example.com/users/999", status=404)
responses.add(responses.GET, "https://api.example.com/health", json={}) # Unnecessary
responses.add(responses.POST, "https://api.example.com/users", json={}) # Unnecessary
# ...
4. Use Dependency Injection for Testability¶
Design your code to accept dependencies, making it easy to inject mocks:
# Production code with DI
class UserService:
def __init__(self, http_client=None):
self.client = http_client or httpx.Client()
def get_user(self, user_id: str) -> dict:
response = self.client.get(f"https://api.example.com/users/{user_id}")
return response.json()
# Test code - inject mock client
@pytest.mark.small
def test_user_service_returns_user(mocker):
mock_client = mocker.Mock()
mock_client.get.return_value.json.return_value = {"id": "123", "name": "Alice"}
service = UserService(http_client=mock_client)
user = service.get_user("123")
assert user["name"] == "Alice"
Frequently Asked Questions¶
Q: If mocks bypass the blocker, how do I know my test is hermetic?¶
The network blocker is a safety net, not your only line of defense. If your test uses proper mocking:
No real network calls occur
The test is deterministic and fast
The test would pass even if the network were disconnected
The blocker catches cases where you forgot to mock, or where mocking failed.
Q: Should I use mocking or change to medium/large tests?¶
Use mocking when:
Testing business logic that happens to call external services
The network call is incidental to what you’re testing
You want fast, deterministic feedback
Change to medium/large when:
Testing the actual integration with an external service
Verifying real network behavior (timeouts, retries, TLS)
Testing with real databases or local services
Q: What if my mock setup fails and a real call gets through?¶
That’s exactly when the network blocker helps. If your mock doesn’t properly intercept a call, the blocker will catch it and fail the test with a clear error message, rather than making a real network call that could be slow, flaky, or have side effects.
Q: Do I need to use @responses.activate or similar decorators?¶
Yes, mocking libraries typically require explicit activation:
Library |
Activation Method |
|---|---|
|
|
|
|
|
|
|
|
|
Automatic via |
|
|
If you forget activation, the real call will go through and the network blocker will catch it.