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-db skips schema rebuild.
  • pytest-xdist parallelizes: 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 — use force_login.
  • Tests sharing state via class attributes.
  • Calling external APIs in tests — use mocks.
  • TIME_ZONE / USE_TZ mismatch 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 .