Troubleshooting Network Violations¶
PLANNED FEATURE - Coming in v0.4.0
This troubleshooting guide describes error messages and behaviors that will be available once network isolation is fully released. The
NetworkBlockerPortinterface exists (PR #74), but pytest hook integration is planned for PR #69. The error messages and CLI options shown below are not yet available.Track progress: Issue #70
This guide helps you identify and fix network access violations in your test suite.
Understanding the Error Message¶
When a network violation occurs in strict mode, you see an error like this:
============================================================
HermeticityViolationError
============================================================
Test: tests/test_api.py::test_fetch_user
Category: SMALL
Violation: Network access attempted
Details:
Attempted connection to: api.example.com:443
Small tests have restricted resource access. Options:
1. Mock the network call using responses, httpretty, or respx
2. Use dependency injection to provide a fake HTTP client
3. Change test category to @pytest.mark.medium (if network is required)
Documentation: See docs/architecture/adr-001-network-isolation.md
============================================================
The error tells you:
Test: The full pytest node ID of the failing test
Category: The test size (SMALL, MEDIUM, etc.)
Details: The host and port that the test attempted to connect to
Options: Suggested fixes for the violation
Common Violation Scenarios¶
1. Direct HTTP Requests¶
Symptom: Connection to an external API hostname on port 443 or 80.
Attempted connection to: api.example.com:443
Cause: Test code directly calls an HTTP library:
import requests
@pytest.mark.small
def test_get_user():
response = requests.get("https://api.example.com/users/123")
assert response.status_code == 200
Fix: Use a mocking library like responses:
import responses
import requests
@pytest.mark.small
@responses.activate
def test_get_user():
responses.add(
responses.GET,
"https://api.example.com/users/123",
json={"id": "123", "name": "Test User"},
status=200,
)
response = requests.get("https://api.example.com/users/123")
assert response.status_code == 200
assert response.json()["name"] == "Test User"
2. DNS Lookups¶
Symptom: Connection to DNS server (port 53) or hostname resolution.
Attempted connection to: 8.8.8.8:53
Cause: Code performs DNS resolution before the connection is blocked:
import socket
@pytest.mark.small
def test_resolve_hostname():
ip = socket.gethostbyname("example.com")
assert ip is not None
Fix: Mock the DNS resolution:
@pytest.mark.small
def test_resolve_hostname(mocker):
mocker.patch("socket.gethostbyname", return_value="93.184.216.34")
ip = socket.gethostbyname("example.com")
assert ip == "93.184.216.34"
3. Database Connections¶
Symptom: Connection to database ports (5432 for PostgreSQL, 3306 for MySQL, etc.).
Attempted connection to: localhost:5432
Cause: Test connects to a real database:
import psycopg2
@pytest.mark.small
def test_database_query():
conn = psycopg2.connect("postgresql://localhost/testdb")
# ...
Fix for small tests: Use an in-memory fake or mock:
@pytest.mark.small
def test_database_query(mocker):
mock_conn = mocker.Mock()
mock_cursor = mocker.Mock()
mock_conn.cursor.return_value = mock_cursor
mock_cursor.fetchall.return_value = [("row1",), ("row2",)]
mocker.patch("psycopg2.connect", return_value=mock_conn)
conn = psycopg2.connect("postgresql://localhost/testdb")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
results = cursor.fetchall()
assert len(results) == 2
Fix for integration tests: Change to medium test:
@pytest.mark.medium # Medium tests can access localhost
def test_database_query():
conn = psycopg2.connect("postgresql://localhost/testdb")
# ...
4. Redis/Cache Connections¶
Symptom: Connection to Redis (port 6379) or Memcached (port 11211).
Attempted connection to: localhost:6379
Cause: Test connects to a cache server:
import redis
@pytest.mark.small
def test_cache_operation():
r = redis.Redis(host="localhost", port=6379)
r.set("key", "value")
Fix for small tests: Use fakeredis:
import fakeredis
@pytest.mark.small
def test_cache_operation():
r = fakeredis.FakeRedis()
r.set("key", "value")
assert r.get("key") == b"value"
Fix for integration tests: Change to medium test.
5. gRPC Connections¶
Symptom: Connection to gRPC service ports.
Attempted connection to: grpc.example.com:50051
Cause: Test calls a gRPC service:
import grpc
@pytest.mark.small
def test_grpc_call():
channel = grpc.insecure_channel("grpc.example.com:50051")
stub = MyServiceStub(channel)
response = stub.GetData(Request())
Fix: Use grpc_testing or mock the stub:
@pytest.mark.small
def test_grpc_call(mocker):
mock_stub = mocker.Mock()
mock_stub.GetData.return_value = Response(data="test")
mocker.patch("my_module.MyServiceStub", return_value=mock_stub)
# Test code that uses the stub
...
6. SMTP/Email Connections¶
Symptom: Connection to mail servers (port 25, 465, 587).
Attempted connection to: smtp.gmail.com:587
Cause: Test sends actual emails:
import smtplib
@pytest.mark.small
def test_send_email():
server = smtplib.SMTP("smtp.gmail.com", 587)
server.send_message(msg)
Fix: Mock the SMTP connection:
@pytest.mark.small
def test_send_email(mocker):
mock_smtp = mocker.Mock()
mocker.patch("smtplib.SMTP", return_value=mock_smtp)
# Call code that sends email
send_notification("user@example.com", "Hello")
mock_smtp.send_message.assert_called_once()
Identifying Network-Calling Code¶
Step 1: Run Tests in Warn Mode¶
First, identify all violations without failing tests:
pytest --test-categories-enforcement=warn 2>&1 | grep -A5 "Network access violation"
Step 2: Add Debugging Output¶
If the source is unclear, add socket debugging:
import socket
# Temporarily patch to see call stack
original_connect = socket.socket.connect
def debug_connect(self, address):
import traceback
print(f"Connection attempt to: {address}")
traceback.print_stack()
return original_connect(self, address)
socket.socket.connect = debug_connect
Step 3: Use pytest Verbose Mode¶
Run the specific test with verbose output:
pytest tests/test_api.py::test_fetch_user -vvs
Step 4: Check Fixture Dependencies¶
Network calls often happen in fixtures:
@pytest.fixture
def api_client():
# This fixture makes a network call!
client = APIClient("https://api.example.com")
client.authenticate() # <-- Network call here
return client
Review all fixtures used by the failing test.
Migration Guide¶
Phase 1: Assessment¶
Enable warn mode in CI:
[tool.pytest.ini_options] test_categories_enforcement = "warn"
Collect all warnings from a full test run
Categorize violations by type (HTTP, database, cache, etc.)
Estimate effort to fix each category
Phase 2: Quick Wins¶
Fix tests that already have mocking infrastructure but make extra calls
Replace real clients with fakes in test fixtures
Add missing
@responses.activatedecorators
Phase 3: Refactoring¶
Introduce dependency injection for HTTP clients
Create test doubles for external service clients
Add factory functions that return appropriate client for context
Phase 4: Enforcement¶
Switch to strict mode in CI:
[tool.pytest.ini_options] test_categories_enforcement = "strict"
Add pre-commit hook to catch violations locally
Document mocking patterns for the team
Temporary Workarounds¶
Recategorize the Test¶
If a test genuinely requires network access, it’s not a small test - recategorize it:
@pytest.mark.medium # Recategorized: requires network access
def test_api_integration():
"""This test needs network access, so it's a medium test."""
...
The test size defines the constraints, not the other way around.
Skip in CI Only¶
For tests that work locally but fail in CI due to network restrictions:
import os
@pytest.mark.skipif(
os.environ.get("CI") == "true",
reason="Network access blocked in CI"
)
@pytest.mark.small
def test_requires_network():
...
This is a temporary measure - fix the underlying issue.
Getting Help¶
If you encounter a violation you cannot resolve:
Check the examples documentation
Review the ADR for network isolation
Open a GitHub Discussion with:
The full error message
The test code (sanitized if needed)
What you have tried