Move from a slow, brittle test suite to a fast, trustworthy one. Master pytest-django fixtures, generate data with factory_boy, find edge cases automatically with property-based testing, and measure real coverage with mutation testing.
A test suite that's slow gets skipped, and one that passes while bugs ship is worse than none — it lulls you. Real confidence comes from fast tests, realistic data, automatic edge-case discovery, and a way to verify your tests actually catch bugs. Here's the modern Django testing stack.
pytest gives you plain assert statements, powerful fixtures, and parametrization. Configure it and use markers for DB access:
pip install pytest-django pytest-xdist
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = djzen.settings
addopts = --reuse-db -n auto # reuse test DB, run on all cores
--reuse-db skips recreating the schema each run; -n auto (pytest-xdist) parallelizes across CPU cores. Together they often cut suite time by 5–10×.
Fixtures rot and hand-built objects are tedious. Factories generate valid, varied objects on demand:
import factory
class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = Order
customer = factory.SubFactory(UserFactory)
total = factory.Faker("pydecimal", left_digits=3, positive=True)
@factory.post_generation
def items(self, create, extracted, **kw):
if create:
OrderItemFactory.create_batch(extracted or 3, order=self)
order = OrderFactory() # fully built, with 3 items
big = OrderFactory(total=Decimal("9999")) # override what you care about
Example-based tests only check the cases you imagined. Hypothesis generates hundreds of inputs and finds the ones you didn't — then shrinks a failure to the minimal reproducing case:
from hypothesis import given, strategies as st
@given(st.decimals(min_value=0, max_value=10**6, places=2),
st.integers(min_value=0, max_value=100))
def test_discount_never_exceeds_total(price, percent):
result = apply_discount(price, percent)
assert 0 <= result <= price # an invariant, for ALL inputs
You assert properties ("discount never makes the price negative or higher than the original") instead of specific numbers. This is where the gnarly edge-case bugs surface.
100% coverage means every line ran, not that a bug in it would be caught. Mutation testing deliberately injects bugs (flips < to <=, + to -) and checks whether your suite fails. A surviving mutant is a blind spot.
pip install mutmut
mutmut run
mutmut results # surviving mutants = untested behavior
A trustworthy suite is fast (pytest with --reuse-db -n auto), fed by realistic factory data, hardened by property-based tests that hunt edge cases you'd never write by hand, and validated by mutation testing that proves the tests actually bite. Coverage tells you what ran; mutation testing tells you what's protected — aim for the latter.