django-test-doctor
360° health check for Django projects. One command, fifteen layers of
analysis, one verdict. Catches crashes your unit tests miss because they
never exercise the blank-init / GET / POST / signal-save / {% url %} /
permission-guard / auto-gen-parse / update_fields-consistency /
fk-redundancy pathway an actual user hits first.
Installed as django-test-doctor, imported as django_doctor:
pip install django-test-doctor
./manage.py doctor
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.
Real-world bugs doctor caught in the past week:
- A form whose
__init__assigns toself.fields['fonction'].choicescrashed withAttributeErroron every GET becauseContact.fonctionis a plainCharField— 500 in production. Fixed by theformscheck. - A
post_savesignal onMandatcreated aConfigurationTVAwithout passing the NOT-NULLregimeFK —IntegrityErroron every mandat creation. Fixed by thepost_smokecheck. - An
OperationTVAForm.clean()that assignedcleaned_data["montant_ttc"]whilemontant_ttcwasn't inMeta.fields— Django silently dropped the value, NOT NULL violation at save. Fixed by theforms_metacheck.
Every one of these passed ./manage.py check, passed its unit test suite,
and still reached production.
Layers
| Layer | What it does | Status |
|---|---|---|
system |
Wraps Django's built-in check framework |
✅ 0.1 |
urls |
Crawls urlpatterns, probes every URL as anon / user / staff / superuser |
✅ 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 exist on the model |
✅ 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 in first-party templates |
✅ 0.6 |
views |
First-party view with no LoginRequired / @login_required / DRF permission |
✅ 0.6 |
autogen |
int(x.split('-')[-1]) auto-gen in Model.save() without try/except |
✅ 0.7 |
update_fields |
obj.save(update_fields=[...]) dropping fields that save() recomputes |
✅ 0.8 |
fk_redundant |
Column duplicating a value reachable via a ForeignKey on the same model | ✅ 0.10 |
i18n |
Untranslated msgid, stale .mo vs .po, missing _() wrapping |
🚧 0.11 |
Plus in 0.6: --html <path> (standalone HTML report suitable for CI
artifacts), --diff <ref> (only show findings on files changed since a
git ref — perfect for PR-scoped gating), and a URL-kwargs fixture
generator that unlocks the urls and post_smoke checks on detail and
edit routes (<int:pk>, <uuid:id>, <slug:slug>).
Quickstart
pip install django-test-doctor
Add to INSTALLED_APPS:
INSTALLED_APPS = [
...,
"django_doctor",
]
Then:
./manage.py doctor # full report
./manage.py doctor --section forms,post_smoke # target specific layers
./manage.py doctor --quick # skip slow layers
./manage.py doctor --ci # exit 1 on critical/error
./manage.py doctor --json # machine-readable output
Safety
post_smoke writes to the database (creates a transient superuser, submits
forms) but always rolls back via transaction.atomic() +
savepoint_rollback. Still, point it at a dev or CI database — never
production.
Configuration
In your project's pyproject.toml:
[tool.django-doctor]
enabled = ["*"]
disabled = [] # or ["post_smoke"] to skip slow layers
fail_on = ["critical", "error"] # what --ci should bail on
ignore = ["urls:admin:*", "forms:legacy_migrations.*"]
[tool.django-doctor.urls]
roles = ["anonymous", "authenticated", "staff", "superuser"]
skip = ["admin:*", "djdt:*", "rp-initiated-logout"]
timeout = 10
[tool.django-doctor.forms]
scenarios = ["blank", "empty_data"]
[tool.django-doctor.migrations]
skip_apps = []
include_third_party = false
[tool.django-doctor.post_smoke]
skip = ["legacy:*"]
[tool.django-doctor.security]
forbidden_secret_keys = ["django-insecure", "changeme", "secret"]
Plugin API
Third-party checks plug in via the django_doctor.checks entry point:
[project.entry-points."django_doctor.checks"]
my_check = "my_package.checks:MyCheck"
# my_package/checks.py
# Distribution is django-test-doctor, import path is django_doctor.
from django_doctor import Check, Finding, Severity
class MyCheck(Check):
id = "my_check"
description = "Verify my custom invariant"
def run(self, project):
for thing in project.iter_something():
if not thing.ok:
yield Finding(
severity=Severity.ERROR,
location=thing.file_path,
message=f"{thing!r} is broken",
fix_hint="Run ./manage.py fix_things",
)
Need auto-generated form fixtures in your own tests? The helper doctor uses internally is public:
from django_doctor.fixtures import make_form_fixture
data = make_form_fixture(MyModelForm)
client.post("/my-view/", data)
Status
Alpha — 0.10 ships fifteen layers + HTML reporter + --diff mode + URL-kwargs fixtures. Roadmap in CHANGELOG.md. Battle-tested
against a production Django 6 / DRF / PostGIS / pgvector codebase with ~175
models, ~150 forms, 202 serializers, and 300+ URLs.
License
MIT © Paul Guindo
Release files for django-test-doctor 0.10.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_test_doctor-0.10.4.tar.gz | 99.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_test_doctor-0.10.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 165.2 kB
Release files / django_test_doctor-0.10.4.tar.gz
| Download URL | django_test_doctor-0.10.4.tar.gz |
|---|---|
| Size | 99.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ae01e21f18453c90962821cd1313f4b6f1d6ab79d508d2fd9e09bd827c6c8879
|
|
BLAKE2b-256 checksum How to use checksums |
6fe2a2ba962c44086cc61df4887db6dc4f1e1d02a1aa19f0b03486a653972aaa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 21, 2026.
Transparency logRelease files / django_test_doctor-0.10.4-py3-none-any.whl
| Download URL | django_test_doctor-0.10.4-py3-none-any.whl |
|---|---|
| Size | 66.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
57e84409777ee61c1ce4f8454320e7416dc8d7d80695484c0f339636d6b118d6
|
|
BLAKE2b-256 checksum How to use checksums |
26f9b355106692d916db2e9574e0ea2b5873a8c56fa95a20729920b1f6fc648c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 21, 2026.
Transparency log