Django testing cheatsheet.
Install
uv add --dev pytest pytest-django factory-boy
pytest.ini or pyproject.toml:
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings"
python_files = ["test_*.py", "*_test.py"]
addopts = "-ra --reuse-db"
Basic test
# blog/tests/test_views.py
import pytest
@pytest.mark.django_db
def test_post_list(client):
r = client.get("/blog/")
assert r.status_code == 200
def test_index(client):
r = client.get("/")
assert r.status_code == 200
@pytest.mark.django_db enables DB access. Use --reuse-db to skip recreation between runs.
Client
def test_form_submission(client):
r = client.post("/posts/new/", {"title": "x", "body": "y"})
assert r.status_code == 302 # redirect after success
def test_login_required(client):
r = client.get("/dashboard/")
assert r.status_code == 302
assert r.url.startswith("/login/")
Authenticated client
@pytest.fixture
def user(db):
return User.objects.create_user(email="[email protected]", password="x")
@pytest.fixture
def auth_client(client, user):
client.force_login(user)
return client
def test_dashboard(auth_client):
r = auth_client.get("/dashboard/")
assert r.status_code == 200
DRF APIClient
from rest_framework.test import APIClient
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def auth_api(api_client, user):
api_client.force_authenticate(user=user)
return api_client
def test_api(auth_api):
r = auth_api.post("/api/posts/", {"title": "x"}, format="json")
assert r.status_code == 201
factory_boy
# blog/tests/factories.py
import factory
from django.contrib.auth import get_user_model
from blog.models import Post
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = get_user_model()
django_get_or_create = ["email"]
email = factory.Sequence(lambda n: f"user{n}@example.com")
username = factory.Sequence(lambda n: f"user{n}")
class PostFactory(factory.django.DjangoModelFactory):
class Meta:
model = Post
title = factory.Faker("sentence")
body = factory.Faker("text")
author = factory.SubFactory(UserFactory)
def test_with_factory(db):
post = PostFactory()
assert post.id is not None
user = UserFactory(email="[email protected]")
posts = PostFactory.create_batch(5, author=user)
Model tests
def test_post_model(db):
p = PostFactory(title="hello")
assert str(p) == "hello"
assert p.author is not None
Form tests
def test_post_form_invalid():
form = PostForm(data={"title": ""})
assert not form.is_valid()
assert "title" in form.errors
def test_post_form_valid():
form = PostForm(data={"title": "x", "body": "y"})
assert form.is_valid()
Mocking external services
from unittest.mock import patch
def test_send_email(client, mailoutbox):
client.post("/contact/", {"email": "[email protected]", "msg": "hi"})
assert len(mailoutbox) == 1
assert mailoutbox[0].subject == "..."
@patch("blog.services.send_via_api")
def test_external(mock_send, client):
mock_send.return_value = {"ok": True}
client.post("/x/", {...})
mock_send.assert_called_once()
mailoutbox is a pytest-django fixture.
Override settings
@pytest.mark.django_db
def test_thing(settings):
settings.DEBUG = False
settings.ALLOWED_HOSTS = ["*"]
...
Or decorator:
from django.test import override_settings
@override_settings(DEBUG=False)
class MyTest(TestCase):
...
Database fixtures
@pytest.fixture
def posts(db):
return PostFactory.create_batch(10)
def test_list(client, posts):
r = client.get("/posts/")
assert len(r.context["posts"]) == 10
transactional tests
By default, each test is wrapped in a transaction (rolled back).
@pytest.mark.django_db(transaction=True)
def test_with_real_commits():
# Useful when testing thread-based code, Celery, etc.
...
Coverage
uv add --dev coverage pytest-cov
uv run pytest --cov=blog --cov-report=html --cov-report=term
.coveragerc:
[run]
source = blog
omit = */migrations/*
branch = true
[report]
fail_under = 80
exclude_lines =
pragma: no cover
raise NotImplementedError
Speeding up tests
--reuse-dbskips schema rebuild.pytest-xdistparallelizes:pytest -n auto.- In-memory SQLite for unit tests; Postgres for integration.
--no-migrations(django-test-without-migrations).
Testing async views
@pytest.mark.django_db
@pytest.mark.asyncio
async def test_async_view(async_client):
r = await async_client.get("/api/x/")
assert r.status_code == 200
Channels (WebSocket) tests
from channels.testing import WebsocketCommunicator
from config.asgi import application
@pytest.mark.asyncio
async def test_ws():
comm = WebsocketCommunicator(application, "/ws/chat/")
connected, _ = await comm.connect()
assert connected
await comm.send_to(text_data="hi")
msg = await comm.receive_from()
assert "hi" in msg
await comm.disconnect()
Common mistakes
- Forgetting
@pytest.mark.django_db→ DB access errors. client.login()without password — useforce_login.- Tests sharing state via class attributes.
- Calling external APIs in tests — use mocks.
TIME_ZONE/USE_TZmismatch causing assertion failures.
Read this next
If you want my pytest + factories starter, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .