Testing¶
Django-Bolt provides a TestClient for testing your API endpoints without starting a server.
TestClient¶
The TestClient routes requests through the Rust layer, providing realistic testing:
from django_bolt import BoltAPI
from django_bolt.testing import TestClient
api = BoltAPI()
@api.get("/hello")
async def hello():
return {"message": "world"}
# Test the endpoint
with TestClient(api) as client:
response = client.get("/hello")
assert response.status_code == 200
assert response.json() == {"message": "world"}
Making requests¶
GET requests¶
with TestClient(api) as client:
# Simple GET
response = client.get("/users")
# GET with query parameters
response = client.get("/search?q=test&limit=20")
# GET with headers
response = client.get("/secure", headers={"Authorization": "Bearer token"})
POST requests¶
with TestClient(api) as client:
# POST with JSON body
response = client.post(
"/users",
json={"name": "John", "email": "john@example.com"}
)
# POST with form data
response = client.post(
"/login",
data={"username": "john", "password": "secret"}
)
PUT, PATCH, DELETE¶
with TestClient(api) as client:
# PUT (full update)
response = client.put(
"/users/1",
json={"name": "Updated", "email": "updated@example.com"}
)
# PATCH (partial update)
response = client.patch("/users/1", json={"name": "Patched"})
# DELETE
response = client.delete("/users/1")
Response object¶
The response object provides access to status, headers, and body:
response = client.get("/users/1")
# Status code
assert response.status_code == 200
# JSON body
data = response.json()
assert data["id"] == 1
# Raw content
raw = response.content # bytes
# Headers
content_type = response.headers.get("content-type")
Testing path parameters¶
@api.get("/users/{user_id}")
async def get_user(user_id: int):
return {"id": user_id, "name": f"User {user_id}"}
with TestClient(api) as client:
response = client.get("/users/123")
assert response.status_code == 200
data = response.json()
assert data["id"] == 123
assert data["name"] == "User 123"
Testing query parameters¶
@api.get("/search")
async def search(q: str, limit: int = 10):
return {"query": q, "limit": limit}
with TestClient(api) as client:
response = client.get("/search?q=test&limit=20")
data = response.json()
assert data["query"] == "test"
assert data["limit"] == 20
Testing headers¶
from typing import Annotated
from django_bolt.param_functions import Header
@api.get("/with-header")
async def with_header(x_custom: Annotated[str, Header()]):
return {"header_value": x_custom}
with TestClient(api) as client:
response = client.get(
"/with-header",
headers={"X-Custom": "test-value"}
)
assert response.json() == {"header_value": "test-value"}
Testing request body¶
import msgspec
class UserCreate(msgspec.Struct):
name: str
email: str
@api.post("/users")
async def create_user(user: UserCreate):
return {"id": 1, "name": user.name, "email": user.email}
with TestClient(api) as client:
response = client.post(
"/users",
json={"name": "John", "email": "john@example.com"}
)
assert response.status_code == 200
data = response.json()
assert data["id"] == 1
assert data["name"] == "John"
Testing error responses¶
with TestClient(api) as client:
# Test 404
response = client.get("/nonexistent")
assert response.status_code == 404
# Test validation error (422)
response = client.post("/users", json={}) # Missing required fields
assert response.status_code == 422
Testing custom status codes¶
@api.post("/created", status_code=201)
async def create():
return {"created": True}
with TestClient(api) as client:
response = client.post("/created")
assert response.status_code == 201
Testing multiple HTTP methods¶
@api.get("/resource")
async def get_resource():
return {"method": "GET"}
@api.post("/resource")
async def create_resource():
return {"method": "POST"}
@api.put("/resource")
async def update_resource():
return {"method": "PUT"}
@api.delete("/resource")
async def delete_resource():
return {"method": "DELETE"}
with TestClient(api) as client:
assert client.get("/resource").json() == {"method": "GET"}
assert client.post("/resource").json() == {"method": "POST"}
assert client.put("/resource").json() == {"method": "PUT"}
assert client.delete("/resource").json() == {"method": "DELETE"}
Streaming responses¶
Test streaming with stream=True:
@api.get("/stream")
async def stream():
async def generate():
for i in range(5):
yield f"data: {i}\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
with TestClient(api) as client:
response = client.get("/stream", stream=True)
assert response.status_code == 200
# Iterate over chunks
chunks = list(response.iter_content(chunk_size=32, decode_unicode=True))
assert len(chunks) > 0
# Or iterate over lines
lines = list(response.iter_lines())
data_lines = [l for l in lines if l.startswith("data:")]
assert len(data_lines) == 5
Testing with pytest¶
Basic test file¶
# tests/test_api.py
import pytest
from django_bolt import BoltAPI
from django_bolt.testing import TestClient
@pytest.fixture
def api():
"""Create fresh API instance."""
api = BoltAPI()
@api.get("/hello")
async def hello():
return {"message": "world"}
return api
@pytest.fixture
def client(api):
"""Create test client."""
with TestClient(api) as client:
yield client
def test_hello(client):
response = client.get("/hello")
assert response.status_code == 200
assert response.json() == {"message": "world"}
Testing with Django database¶
import pytest
from django_bolt import BoltAPI
from django_bolt.testing import TestClient
from myapp.models import User
@pytest.fixture
def api():
api = BoltAPI()
@api.get("/users/{user_id}")
async def get_user(user_id: int):
user = await User.objects.aget(id=user_id)
return {"id": user.id, "username": user.username}
return api
@pytest.mark.django_db(transaction=True)
def test_get_user(api):
# Create test data
user = User.objects.create(username="testuser", email="test@example.com")
with TestClient(api) as client:
response = client.get(f"/users/{user.id}")
assert response.status_code == 200
assert response.json()["username"] == "testuser"
Testing ViewSets¶
import pytest
from django_bolt import BoltAPI, ViewSet
from django_bolt.testing import TestClient
from myapp.models import Article
@pytest.fixture
def api():
api = BoltAPI()
@api.viewset("/articles")
class ArticleViewSet(ViewSet):
queryset = Article.objects.all()
async def list(self, request):
articles = []
async for article in await self.get_queryset():
articles.append({"id": article.id, "title": article.title})
return articles
async def retrieve(self, request, pk: int):
article = await self.get_object(pk)
return {"id": article.id, "title": article.title}
return api
@pytest.mark.django_db(transaction=True)
def test_article_viewset(api):
Article.objects.create(title="Test Article", content="Content")
with TestClient(api) as client:
# Test list
response = client.get("/articles")
assert response.status_code == 200
assert len(response.json()) == 1
# Test retrieve
response = client.get("/articles/1")
assert response.status_code == 200
assert response.json()["title"] == "Test Article"
AsyncTestClient¶
For async test functions, use AsyncTestClient:
import pytest
from django_bolt import BoltAPI
from django_bolt.testing import AsyncTestClient
api = BoltAPI()
@api.get("/hello")
async def hello():
return {"message": "world"}
@pytest.mark.asyncio
async def test_async():
async with AsyncTestClient(api) as client:
response = await client.get("/hello")
assert response.status_code == 200
assert response.json() == {"message": "world"}
The AsyncTestClient is useful when:
- Your test function is async
- You need to await other async operations in the same test
- You're using
pytest-asyncio
Test isolation¶
Each TestClient context creates isolated state:
def test_isolated():
api = BoltAPI()
@api.get("/counter")
async def counter():
return {"count": 1}
# Each with block is isolated
with TestClient(api) as client:
response = client.get("/counter")
assert response.json()["count"] == 1
with TestClient(api) as client:
response = client.get("/counter")
assert response.json()["count"] == 1 # Fresh state
Best practices¶
-
Use fixtures: Create API and client fixtures for reuse.
-
Test all status codes: Verify success (2xx), client errors (4xx), and server errors (5xx).
-
Test validation: Ensure invalid input returns 422 with proper error messages.
-
Test authentication: Verify endpoints require proper auth when expected.
-
Use
django_dbmark: For tests that access the database. -
Clean up test data: Use database transactions that roll back.