django-test-doctor¶
360° health check for Django projects. One command, fifteen layers of analysis, one verdict.
Distribution vs import path
Installed as django-test-doctor on PyPI, but the Python import path
is django_doctor. pip install django-test-doctor, then
from django_doctor import Check, Finding.
Why¶
./manage.py check validates runtime configuration. djlint validates
template syntax. pytest validates whatever you remembered to test.
Nothing validates the whole project at once. Doctor does.
Three real bugs doctor surfaced in the past week on a production codebase (Django 6 / DRF / PostGIS, ~175 models, ~150 forms, 202 serializers):
formscheck caught aModelForm.__init__that touchedself.fields['fonction'].choiceswherefonctionwas a bareCharFieldwith no choices →AttributeError→ HTTP 500 on every GET.post_smokecheck caught apost_savesignal creating aConfigurationTVAwithout its NOT-NULLregimeFK →IntegrityError→ HTTP 500 on every/mandats/nouveau/POST.forms_metacheck caught anOperationTVAForm.clean()that assigned tocleaned_data["montant_ttc"]whilemontant_ttcwasn't inMeta.fields→ Django silently dropped the value →IntegrityErroron save.
Every one of these passed ./manage.py check, passed their unit tests,
and still reached production.
Layers¶
| Layer | What it does | Status |
|---|---|---|
system |
Wraps Django's built-in check framework |
✅ 0.1 |
urls |
Probes every URL as anon / user / staff / superuser, fails on 5xx | ✅ 0.1 |
forms |
Instantiates every Form / ModelForm blank + with empty data |
✅ 0.1 |
migrations |
Byte-for-byte parity with makemigrations --check --dry-run |
✅ 0.1 |
admin |
list_display / list_filter / search_fields / ordering fields actually exist |
✅ 0.2 |
models |
__str__ defined, Meta.ordering, no SET_NULL on NOT NULL FK |
✅ 0.2 |
security |
check --deploy + weak SECRET_KEY + DEBUG=True + empty ALLOWED_HOSTS |
✅ 0.2 |
drf |
Serializer init + Meta.fields ↔ Meta.model coherence |
✅ 0.3 |
post_smoke |
POST every CreateView / UpdateView with auto fixture, fail on 5xx |
✅ 0.4 |
forms_meta |
cleaned_data[x] = … where x is NOT NULL but not in Meta.fields |
✅ 0.5 |
templates |
Broken {% url %} / {% static %} / template syntax |
✅ 0.6 |
views |
First-party views with no LoginRequired / @login_required / DRF permission |
✅ 0.6 |
autogen |
Unsafe int(x.split('-')[-1]) in auto-gen without try/except |
✅ 0.7 |
update_fields |
save(update_fields=…) dropping fields recomputed by save() |
✅ 0.8 |
fk_redundant |
Column duplicating a value reachable via a ForeignKey on the same model | ✅ 0.10 |
Plus in 0.6: --html <path> (standalone HTML report), --diff <ref>
(only show findings on files changed since a git ref), and URL-kwargs
fixtures that unlock urls / post_smoke on detail and edit routes.
Roadmap: static, i18n, --fix, --watch.
Next steps¶
Status¶
Alpha. Battle-tested against a production Django 6 / DRF / PostGIS / pgvector codebase. Fifteen checks, 72 regression tests, Trusted Publishing on PyPI. Still expect the occasional rough edge — report issues on GitHub.