Skip to content

Latest commit

Β 

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

django-admin-tests

CI

Automatic admin smoke-test coverage for Django projects.

πŸ“– Documentation

Every ModelAdmin you register gets its changelist, add and change views asserted to return 200, and its add and change forms submitted back β€” as part of your test run, under either manage.py test or pytest. Admin pages break quietly: a renamed field in list_display, a get_queryset that blows up on a related lookup, a save_model that raises. This catches that without you writing a test per model.

You get one test method per model and view, named after the model, so a broken admin fails under its own name rather than inside a shared loop:

test_admin_smoke_shop_product_changelist
test_admin_smoke_shop_product_add
test_admin_smoke_shop_product_change
test_admin_smoke_shop_product_add_post
test_admin_smoke_shop_product_change_post
test_admin_smoke_blog_article_changelist
...

What the POST checks catch

A GET only proves the form renders. The _add_post and _change_post methods submit it, which is the only way to reach:

  • save_model() / save_related() raising
  • a clean() that crashes rather than merely rejecting
  • a readonly_fields mismatch that only bites on save
  • a field required by the database but missing from the form
  • inline formsets that don't round-trip
  • post_save signals that break

The payload is built from your admin's own form β€” fields, fieldsets, exclude, a custom form and readonly_fields are all honoured exactly as the real view honours them. The change form round-trips an existing object; the add form is filled in automatically.

A successful admin POST is a 302 redirect. A 200 means the form came back with errors, and the failure message reports them.

Note: ADMIN_TESTS_ALLOWED_STATUS_CODES applies to GETs only. It defaults to {200}, which is the failure signal for a POST, so applying it there would invert the check. Per-model overrides (ADMIN_TESTS_MODEL_ALLOWED_STATUS_CODES, model_allowed_status_codes) do apply to both.

A successful add POST creates a row and fires its signals. Database writes roll back with the test's transaction, but signals are not transactional β€” exclude any model whose post_save reaches an external system.

Install

pip install django-admin-tests

Requires Python 3.10+ and Django 4.2+.

Usage

There are two ways in. Both run under either test runner.

Option 1 β€” subclass it (works everywhere)

Put this in any test module your runner already collects:

from django_admin_tests import testcases


class AdminSmokeTest(testcases.AdminSmokeTestCase):
    pass

That's it. Subclassing is also how you customize behavior:

from django_admin_tests import testcases

from myapp.models import InternalReport, LegacyThing


class AdminSmokeTest(testcases.AdminSmokeTestCase):
    # Admins that intentionally deny access:
    model_allowed_status_codes = {InternalReport: {403}}
    # Admins to skip entirely:
    excluded_models = {LegacyThing}

Note: the examples import the module on purpose. Test discovery scans the whole module namespace for TestCase subclasses, so from django_admin_tests.testcases import AdminSmokeTestCase collects the un-customized base class as a test of its own, alongside your subclass. Going through testcases.AdminSmokeTestCase avoids that.

Option 2 β€” pytest plugin (no test file needed)

If you use pytest, the bundled plugin can collect the smoke tests without you writing anything. It's opt-in β€” installing this package will never silently add tests to your suite:

# pyproject.toml
[tool.pytest.ini_options]
django_admin_tests_auto = true

Since there's no class to subclass in this mode, configure it through Django settings instead (see below).

Settings

Setting Default Purpose
ADMIN_TESTS_ALLOWED_STATUS_CODES {200} Globally accepted response statuses
ADMIN_TESTS_MODEL_ALLOWED_STATUS_CODES {} Per-model overrides, keyed "app_label.ModelName"
ADMIN_TESTS_EXCLUDE [] Models to skip entirely, same key format
# settings.py
ADMIN_TESTS_MODEL_ALLOWED_STATUS_CODES = {"myapp.InternalReport": [403]}
ADMIN_TESTS_EXCLUDE = ["myapp.LegacyThing"]

Resolution order is class attribute β†’ Django setting β†’ built-in default, so a subclass always wins over settings.

Excluded models keep their test methods and report as skipped, so an opt-out stays visible in your output rather than looking like a model that was never covered.

Running one model's tests

Because each model gets its own methods, you can iterate on a single admin without re-running the rest:

pytest -k shop_product
python manage.py test myapp.tests.AdminSmokeTest.test_admin_smoke_shop_product_changelist

You can also override a single model's check by defining a method with the generated name β€” yours wins, and the generated one isn't installed:

class AdminSmokeTest(testcases.AdminSmokeTestCase):
    def test_admin_smoke_shop_product_changelist(
        self,
    ): ...  # your own assertions for this one admin

Change views need an object

To load a change view, there has to be something to change. An instance is resolved per model in this order:

  1. A factory you registered for that model
  2. Any existing row
  3. A minimal instance built automatically from the model's fields

If none of those work β€” most often a required self-referential or circular foreign key β€” that model's change-view check is skipped with a warning, not failed. Register a factory to cover it:

# In your AppConfig.ready(), conftest.py, or anywhere that runs at startup
from django_admin_tests import register_factory

from myapp.models import Tricky


def make_tricky():
    return Tricky.objects.create(...)


register_factory(Tricky, make_tricky)

The auto-builder is in-house and dependency-free β€” installing this package pulls in nothing but Django.

Add forms that can't be filled in automatically

The add-view POST has nothing to round-trip, so every value is synthesised from your form's fields. A wrong guess would fail your admin for our mistake, so the payload is validated against the admin's own form before it's sent. If it doesn't validate, the model is skipped, never failed β€” with a warning naming the fields that defeated it:

Skipping Order add POST: could not synthesise a payload its admin form
accepts (customer: This field is required.). Register one with
django_admin_tests.register_add_payload(Order, ...) to cover it.

Supply the payload yourself to cover it:

from django_admin_tests import register_add_payload

from myapp.models import Order


register_add_payload(Order, lambda: {"customer": 1, "total": "9.99"})

Confirmation-field pairs are handled for you: every field rendered with a PasswordInput gets the same value, so auth.User and similar forms validate without a registered payload.

Turning it off

The tests are tagged django_admin_tests:

python manage.py test --exclude-tag=django_admin_tests
pytest -m "not django_admin_tests"

Custom user models

The test client authenticates as a superuser it creates itself, using the default User fields. If your AUTH_USER_MODEL needs something else, supply a factory:

class AdminSmokeTest(testcases.AdminSmokeTestCase):
    user_factory = staticmethod(my_superuser_factory)

Custom admin sites

from myproject.admin import my_site


class AdminSmokeTest(testcases.AdminSmokeTestCase):
    admin_site = my_site

Known limitations

  • Models whose change view can't be instantiated are skipped with a warning rather than failed. That's deliberate β€” a smoke test shouldn't fail because a model is awkward to construct β€” but it does mean an un-covered model can go unnoticed. The warning names it.
  • The auto-builder doesn't try to satisfy custom save()/clean() requirements. Use register_factory for those, or register_add_payload for add forms.
  • A field named in fields/fieldsets but kept out of the form β€” populated in save(), by a signal, or by a hook β€” is treated as valid and passes. Django raises while rendering that admin, so there's no response to check and the GET's status isn't asserted; the POST checks cover those models instead.
  • A required CharField/TextField excluded from the form saves as '' rather than failing. Django passes excluded fields to full_clean(exclude=...), so blank is never checked and the database accepts the empty string. Only relations and non-string NOT NULL columns surface as an error.
  • Many-to-many fields are submitted empty on the add POST β€” selecting rows could violate through-model or business constraints.
  • Custom AUTH_USER_MODEL support is the user_factory hook only; there's no auto-detection of required fields.
  • The admin registry is read when the test class is created, so admins registered after that aren't covered. In practice registration happens during app loading, which always precedes test module import.
  • manage.py test --parallel won't spread these across processes. Django distributes work per TestCase class, and all the generated methods live on one class, so the run stays single-process.
  • Two models whose app label and model name differ only in where the underscore falls (foo_bar.baz vs foo.bar_baz) would generate the same method name. That raises ImproperlyConfigured rather than silently dropping one β€” exclude one of them to proceed.

Releasing

Versions are derived from git tags via hatch-vcs β€” nothing to bump in pyproject.toml. To cut a release:

  1. In CHANGELOG.md, retitle ## [Unreleased] to ## [X.Y.Z] - YYYY-MM-DD and add a fresh empty ## [Unreleased] section above it.
  2. Commit that change, then tag the commit with the bare version β€” no v prefix β€” and push both: git tag X.Y.Z && git push origin master --tags.
  3. Pushing the tag triggers .github/workflows/release.yml, which builds the sdist/wheel, publishes to PyPI (via Trusted Publishing), and creates a GitHub Release using that CHANGELOG section as its notes.

License

MIT β€” see LICENSE.

About

Test suite for your all your admin pages. Works out of the box with minimum configuration and dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages