Pytest Mocking for API Tests — Isolating External Dependencies
In this tutorial, you will learn about Pytest Mocking for API Tests. We cover key concepts, practical examples, and best practices to help you master this topic.
Pytest mocking replaces external dependencies with controlled test doubles, allowing API tests to run without network access, real databases, or third-party services while verifying correct interactions.
Code Example: Mocking HTTP Calls with responses
import pytest
import responses
import httpx
class TestExternalThreatIntel:
@responses.activate
def test_enrich_threat_with_external_api(self, auth_headers):
# Mock the external threat intelligence API
responses.add(
responses.GET,
"https://threat-intel.example.com/api/v1/ip/185.220.101.1",
json={
"ip": "185.220.101.1",
"reputation": "malicious",
"categories": ["malware", "c2"],
"last_seen": "2026-06-28T10:00:00Z",
"confidence": 95
},
status=200
)
# Make the API call that uses the external service
response = httpx.post(
f"{BASE_URL}/api/v1/threats/enrich",
json={"ip": "185.220.101.1"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["reputation"] == "malicious"
assert data["confidence"] == 95
# Verify the external API was called
assert len(responses.calls) == 1
assert responses.calls[0].request.url == \
"https://threat-intel.example.com/api/v1/ip/185.220.101.1"
@responses.activate
def test_external_api_failure_handling(self, auth_headers):
# Mock external API failure
responses.add(
responses.GET,
"https://threat-intel.example.com/api/v1/ip/185.220.101.1",
status=503,
json={"error": "Service unavailable"}
)
# API should handle failure gracefully
response = httpx.post(
f"{BASE_URL}/api/v1/threats/enrich",
json={"ip": "185.220.101.1"},
headers=auth_headers
)
# Should return a partial result or fallback
assert response.status_code == 200
data = response.json()
assert data.get("reputation") is None
assert data.get("fallback", False) == True
Code Example: Mocking with respx (httpx-native)
import respx
from httpx import Response
class TestAsyncThreatIntel:
@respx.mock
async def test_async_external_lookup(self, async_client, auth_headers):
# Mock the external API route
route = respx.get(
"https://threat-intel.example.com/api/v1/ip/10.0.0.1"
).mock(
return_value=Response(200, json={
"ip": "10.0.0.1",
"reputation": "suspicious",
"score": 65
})
)
response = await async_client.post(
"/api/v1/threats/enrich",
json={"ip": "10.0.0.1"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["score"] == 65
assert route.called
Code Example: Mocking Database with unittest.mock
from unittest.mock import patch, MagicMock
from api.repositories import ThreatRepository
class TestThreatRepository:
@patch("api.repositories.db_session")
def test_find_by_id(self, mock_db):
# Configure mock
mock_query = MagicMock()
mock_db.query.return_value = mock_query
mock_query.filter.return_value = mock_query
mock_query.first.return_value = {
"id": "507f1f77bcf86cd799439011",
"name": "Mocked Threat",
"severity": "high"
}
repo = ThreatRepository(mock_db)
result = repo.find_by_id("507f1f77bcf86cd799439011")
assert result["name"] == "Mocked Threat"
mock_db.query.assert_called_once()
@patch("api.repositories.db_session")
def test_save_threat(self, mock_db):
mock_db.add.return_value = None
mock_db.commit.return_value = None
repo = ThreatRepository(mock_db)
threat = {"name": "New Threat", "severity": "low"}
repo.save(threat)
mock_db.add.assert_called_once()
mock_db.commit.assert_called_once()
Common Mistakes
1. Mocking Too Broadly
Mock at the boundary of your system (HTTP calls, database). Mocking internal functions creates brittle tests that break on Refactoring.
2. Not Verifying Mock Interactions
Mocks that silently return default values may hide bugs. Assert that mocks were called with expected arguments using assert_called_once_with().
3. Mocking What You Do Not Own
Mocking third-party library internals is fragile. Mock at the API boundary — mock HTTP responses (responses/respx) rather than the library functions.
4. Leaking Mock State Between Tests
Mocks persist across tests if not reset. Use @responses.activate decorator (auto-cleanup) or reset mocks in teardown.
5. Hardcoded Mock Return Values
Return values should match what the real system returns. Use realistic data structures and status codes. Inconsistent mocks lead to false confidence.
Practice Questions
- Why mock at the system boundary rather than internal functions?
- What is the difference between responses and respx?
- How do you verify a mock was called with specific arguments?
- Why should mock return values be realistic?
- How do you handle external API failures in tests?
Answers:
- Internal function mocks break when the implementation is refactored. System boundary mocks (HTTP, database) test the integration between your code and external services.
- responses patches the requests library. respx patches httpx (both sync and async). Choose based on which HTTP library your application uses.
- Use mock.assert_called_once_with(arg1, arg2, key=value) for exact argument matching. Use mock.assert_called() for just verifying it was called.
- If mocks return unrealistic data, tests pass but the application fails with real data. Use actual response examples or schemas for mock data.
- Mock the external API to return 4xx/5xx status codes and verify your application handles errors gracefully (timeouts, retries, fallbacks, error responses).
Challenge: Build a mocked test suite for a threat intelligence API that integrates with three external services (IP reputation, domain reputation, malware hash lookup). Mock all three external APIs with both success and failure scenarios.
FAQ
Mini Project
Build a mocked test suite for a threat intelligence API with: three mocked external threat intel APIs (success, failure, timeout scenarios), mocked database operations, mocked authentication service, and interaction verification for all mocked calls.
What's Next
Now learn about Load Testing with K6 for Performance Testing your API under load.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro