Pytest for API Testing Deep Dive — Testing Python APIs with httpx and pytest
In this tutorial, you will learn about Pytest for API Testing Deep Dive. We cover key concepts, practical examples, and best practices to help you master this topic.
Pytest with httpx provides a modern, async-native approach to API testing in Python, combining pytest's powerful fixture system and parametrization with httpx's fast HTTP client.
Code Example: Basic API Tests with httpx
import pytest
import httpx
BASE_URL = "https://api.durga-antivirus.com"
class TestThreatAPI:
@pytest.fixture
def client(self):
return httpx.Client(base_url=BASE_URL, timeout=30.0)
@pytest.fixture
def auth_headers(self, client):
resp = client.post("/api/auth/login", json={
"username": "test-analyst",
"password": "test-password"
})
assert resp.status_code == 200
token = resp.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
def test_list_threats(self, client, auth_headers):
response = client.get("/api/v1/threats", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert "threats" in data
assert "total" in data
def test_create_threat(self, client, auth_headers):
response = client.post(
"/api/v1/threats",
json={
"name": "Pytest Integration Test",
"severity": "medium",
"source_ip": "10.0.0.55"
},
headers=auth_headers
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Pytest Integration Test"
assert "id" in data
Code Example: Parametrized Test Cases
@pytest.mark.parametrize("name,severity,source_ip,expected_status", [
("SQL Injection", "high", "192.168.1.1", 201),
("XSS Attack", "medium", "10.0.0.50", 201),
("", "low", "10.0.0.1", 422), # Empty name
("Valid Name", "", "10.0.0.1", 422), # Empty severity
("Long Name " + "x" * 200, "low", "10.0.0.1", 422), # Too long name
("Valid", "low", "not-an-ip", 422), # Invalid IP
])
def test_threat_creation_parameterized(
client, auth_headers, name, severity, source_ip, expected_status
):
payload = {"name": name, "severity": severity, "source_ip": source_ip}
# Remove empty values to test missing field validation
payload = {k: v for k, v in payload.items() if v}
response = client.post(
"/api/v1/threats",
json=payload,
headers=auth_headers
)
assert response.status_code == expected_status
if expected_status == 201:
data = response.json()
assert data["name"] == name
assert "id" in data
Code Example: Async httpx Client
import pytest
import httpx
import asyncio
class TestAsyncThreatAPI:
@pytest.fixture
async def async_client(self):
async with httpx.AsyncClient(
base_url=BASE_URL, timeout=30.0
) as client:
yield client
@pytest.mark.asyncio
async def test_async_threat_creation(self, async_client):
response = await async_client.post(
"/api/v1/threats",
json={
"name": "Async Threat Test",
"severity": "high",
"source_ip": "10.0.0.99"
},
headers={"Authorization": "Bearer test-token"}
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Async Threat Test"
@pytest.mark.asyncio
async def test_concurrent_threat_creation(self, async_client):
threats = [
{"name": f"Concurrent Threat {i}", "severity": "low",
"source_ip": f"10.0.0.{i}"}
for i in range(10)
]
tasks = [
async_client.post(
"/api/v1/threats",
json=t,
headers={"Authorization": "Bearer test-token"}
)
for t in threats
]
responses = await asyncio.gather(*tasks)
for resp in responses:
assert resp.status_code == 201
Common Mistakes
1. Using Requests Instead of httpx
requests is synchronous and blocks the event loop. Use httpx for async tests and better performance with connection pooling.
2. Not Using Pytest Fixtures for Auth
Duplicating login logic in every test creates maintenance overhead. Use a fixture that returns auth headers and reuse it.
3. Hardcoding Test Data
Use pytest parametrization for multiple test scenarios. Hardcoded values make it hard to add new test cases.
4. No Response Time Assertions
APIs that return correct data but are too slow degrade user experience. Add response time assertions: assert response.elapsed.total_seconds() < 1.0.
5. Not Testing Error Responses
Test that the API returns proper error structures for 4xx and 5xx responses. Validate error message format and status codes.
Practice Questions
- Why is httpx preferred over requests for API testing?
- How does pytest parametrization reduce test code duplication?
- How do async API tests improve performance?
- What is the purpose of the pytest fixture scope?
- How do you generate HTML test reports with pytest?
Answers:
- httpx supports async/await, connection pooling, HTTP/2, and has a more modern API. It is faster for concurrent requests and integrates better with async applications.
- Parametrization runs the same test function with different inputs. A single parametrized test replaces multiple similar test functions.
- Async tests can run concurrent requests without blocking. 10 concurrent requests complete in the time of 1 sequential request.
- Fixture scope (function, class, module, session) controls how often the fixture is created. Session-scoped fixtures (auth token) improve performance by creating once.
- Use pytest --html=report.html. Install pytest-html: pip install pytest-html. HTML reports include test names, status, duration, and error details.
Challenge: Build a comprehensive pytest test suite for a FastAPI threat intelligence API with async httpx client, parametrized test cases, fixture-based authentication, response time assertions, and HTML test reporting.
FAQ
Mini Project
Build a pytest test suite for a FastAPI threat intelligence API with: async httpx client, fixture-based auth, parametrized test cases (20+ scenarios), concurrent request tests, response time assertions, and HTML coverage reports.
What's Next
Now learn about Pytest Fixtures for API Testing for building reusable test components.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro