A backend without tests is a backend you are afraid to change. After keeping a Django codebase at full backend coverage with pytest, here is the setup that made tests fast enough to actually run, and useful enough to actually trust.
First principles
A test is a small program that runs your code and checks one claim about what it does. Its value is the bugs it catches. Its cost is the time it takes to run, plus the time it takes to keep up to date as the code changes.
Two things follow. A test that is slow to run stops being run, so speed is part of usefulness, not a nicety. And a test that runs code without checking a result catches nothing, however much coverage it adds.
So the setup below aims at three things: tests that check behavior a caller cares about, tests that are cheap to write and maintain, and a suite fast enough to run on every change.
The setup
Install pytest-django and point it at the settings module in pytest.ini.
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = tests.py test_*.py *_tests.py
The database fixture
Tests do not touch the database unless they ask to. The django_db mark grants access, which keeps pure unit tests honest and fast.
import pytest
@pytest.mark.django_db
def test_order_defaults_to_received():
order = Order.objects.create(reference="PO-1")
assert order.status == "received"
Factories over fixtures of data
Hand-built test rows drift out of date. A factory builds a valid object with sensible defaults and lets each test override only the field it cares about.
import factory
class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = Order
reference = factory.Sequence(lambda n: f"PO-{n}")
status = "received"
Test the API through the client
For application programming interface (API) endpoints, test through the framework’s test client. It exercises routing, authentication, serialization, and the view together, which is what a real caller hits. That is where the bugs that matter live.
Assert the query count
The django_assert_num_queries fixture turns a performance regression into a failing test. If a change adds a hidden query, the test catches it before production does.
Coverage that helps
Coverage measures which lines ran, not whether behavior was verified. Chasing a percentage for its own sake produces tests that execute code and assert nothing. Spend the effort on the unhappy paths: the rejected input, the permission denied, the conflicting update.
Keep the suite fast
Reuse the test database between runs, build objects in memory when a test does not need them saved, and avoid the database entirely for logic that does not require it. A suite that runs in seconds gets run. A suite that takes ten minutes gets skipped.