# django-test-plus replaces Django's TestCase with one that knows your URL names

> django-test-plus is a BSD licensed Django testing helper from REVSYS that adds URL reversing, status assertions, context checks, login helpers and query count assertions to Django's TestCase, with a pytest fixture for the same helpers. It suits a Django team that wants less boilerplate in view tests, and it is a drop-in subclass rather than a separate test runner.

**revsys/django-test-plus** — Useful additions to Django's default TestCase

- Repository: https://github.com/revsys/django-test-plus
- Website: https://django-test-plus.readthedocs.io
- Stars: 631 · Forks: 63
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/revsys-django-test-plus

## The problem is boilerplate, and the fix is a different TestCase

django-test-plus is a set of useful additions to Django's default TestCase, published on PyPI as django-test-plus and developed by REVSYS. The homepage for the documentation is django-test-plus.readthedocs.io.

The stated rationale is honest about the motivation. Writing tests is not always fun, and part of the reason is all of the boilerplate you end up writing. The library is an attempt to cut down on some of that. The documentation even jokes that it will increase the time before you get carpal tunnel by at least three weeks.

The mechanism is deliberately small. You inherit from test_plus.test.TestCase instead of django.test.TestCase, and you get a set of extra methods on the test case object.

```python
from test_plus.test import TestCase

class MyViewTests(TestCase):
    ...
```

That single import is the whole integration. There is no new runner, no plugin to register, no settings to add. Your existing test suite keeps using the Django test command, and every test class that already inherited from django.test.TestCase still works.

That last point is what makes adoption cheap. The library is additive, and the project metadata lists no runtime dependencies at all, with dependencies set to an empty list, so nothing new enters your dependency tree when you install it.

## The helpers cover requests, status codes, context and content

The usage example in the project documentation shows the shape of the API in one test class.

```python
class MyViewTests(TestCase):

    def test_the_view(self):
        # GET a named URL, with args or kwargs if it needs them
        self.get('my-url-name')
        self.response_200()

        # The last response and its context are stored for you
        self.assertInContext('some-key')
        self.assertResponseContains('<p>Hello, World!</p>')

        # POST data, then check the redirect
        self.post('my-form-view', data={'name': 'Test'})
        self.response_302()
```

The mechanics are worth understanding rather than just copying. The get and post helpers take a URL name rather than a path, and they reverse it for you, with arguments or keyword arguments if the route needs them. The last response and its context are stored on the test case, so the status and content assertions read that stored state instead of taking the response as an argument.

So the boilerplate being removed is concrete: a reverse call with a path, a response object you pass to assertEqual, a context lookup written out as response.context['key']. The assertions are named after what they check, response_200, response_302, assertInContext, assertResponseContains, and each one is a single call.

The method reference in the documentation is organised into requests, status code assertions, response and context helpers, and make_user.

## Authentication helpers and the login context manager

Testing a protected view means two things: proving the view redirects an anonymous user, and proving it works for a logged in one. django-test-plus gives you both in a form that reads as one intention.

```python
    def test_auth(self):
        user = self.make_user('u1')

        self.assertLoginRequired('my-protected-view')

        with self.login(user):
            self.get_check_200('my-protected-view')
```

make_user creates a user for the test, assertLoginRequired checks that a URL name is protected, and the login context manager installs the authenticated session for the duration of the block. get_check_200 does the request and the status assertion in one call.

The context manager form is the detail that matters. Django's own test client has client.force_login, which leaves the client logged in for the rest of the test, and that pattern leaks: a test that logs in a user and then asserts something about a different view may be asserting against the wrong session. Wrapping the authenticated part in a with block makes the scope of the session explicit.

The documentation has a dedicated page for these helpers, auth_helpers, covering assertLoginRequired and the login() context, and there is a separate page for testing class based views, which is where the CBV specific helpers live.

## Query count assertions catch the N+1 before production does

One feature is worth singling out, because it is the one that changes how you write code rather than only how you test it.

```python
    def test_query_count(self):
        # 200 OK in under 50 queries, in one line
        self.assertGoodView('my-url-name')
```

assertGoodView checks both that the view returns 200 and that it does so within a query budget. The underlying assertion is assertNumQueriesLessThan, and the documentation has a page for it under low query counts, alongside assertGoodView.

This is a different kind of test helper from the status code assertions. Those one tell you the view works. This one tells you the view will still work when the table has a million rows instead of ten, because a lazy relation in a template or a serializer turns into one query per row. A test that asserts a fixed number of queries turns a performance regression into a red test rather than a support ticket.

The threshold is a number you pick, and the 50 in the example is the documentation's illustration rather than a recommendation for your project. The number has to reflect what your view is supposed to cost, which means the assertion is only as good as the budget you set for it.

## A pytest fixture, a project subclass and other integration points

The library works outside the unittest style too. There is a pytest fixture, and asking for tp gives you the same helpers.

```python
def test_url_reverse(tp):
    assert tp.reverse('api') == '/api/'
```

The plugin is registered through the standard entry point mechanism, with a pytest11 entry point pointing at test_plus.plugin, so it is available once the package is installed. The test extras pull in factory-boy, pytest, pytest-cov and pytest-django, and the documentation notes that the usage page covers pytest fixtures and testing DRF views, so the fixture is not an afterthought.

The other integration point the documentation recommends is your own subclass.

```python
# myproject/test.py
from test_plus.test import TestCase as PlusTestCase

class TestCase(PlusTestCase):
    pass
```

The documentation is explicit that you are encouraged to create your own sub-class for your project so you can add project specific helper methods. That is the right pattern, because it gives you one place to add a helper for your own views and one import for the whole test suite, rather than a mixin in half your test files.

There is also a page on disabling logging, which is a separate concern: a test suite that prints log output to the console is hard to read, and turning logging off is a common request in Django projects.

## Support matrix, build tooling and the maintenance picture

The support statement is specific and worth checking before you pin a version. Python 3.11, 3.12, 3.13, 3.14 and 3.15 are supported, including the free-threaded builds 3.14t and 3.15t. Django 4.2 LTS, 5.1, 5.2 LTS, 6.0 and 6.1 are supported. The project metadata agrees, with requires-python at 3.11 or newer, and a classifier for free threading at level 3, stable.

That matrix is the practical constraint. A team on Python 3.10 or Django 3.2 is out of scope, and the packaging metadata marks the project as Development Status 5, Production/Stable.

The release history is short in the listed metadata, with 2.7.0 on 2026-10-13 as the only release shown, and the last push on the same day. The repository is not archived, and there is a CHANGELOG.md at the root.

The build and release tooling is where this project is most modern. The build backend is hatchling, the repository uses nox for test sessions, a justfile provides the developer commands, and there is a uv.lock file. The justfile runs bumpver for version bumps, a lock target for the uv lock file, a release target that builds with uv, pushes the tags and publishes to PyPI, and separate sessions for tests with Django REST Framework, tests in the current environment, and tests on the latest Python and Django versions. The development instructions in the documentation are two pip install commands, one with the test extra, and nox to run the suite, with a note that the test_project directory at the repository root has to be on the virtualenv path.

The licence is BSD-3-Clause, with the LICENSE file at the root and a matching classifier.

## Conclusion

django-test-plus fits a Django team that writes a lot of view tests and is tired of repeating the same reverse, status code and context boilerplate, since it is a TestCase subclass you adopt in one import. It does not fit a project that wants to keep its test suite free of third party helpers, and it does not fit a team on an older Django, since the support matrix starts at Django 4.2 LTS and Python 3.11. Before adopting it, create your own subclass of test_plus.test.TestCase in your project so project specific helpers live in one place, check that your Django version and Python version are inside the supported matrix, and read the low query counts page, because assertGoodView is the assertion most likely to change how you write a view.

## FAQ

### How do I install django-test-plus and use it in a Django test?

Install it with pip install django-test-plus, then inherit from test_plus.test.TestCase rather than django.test.TestCase in your test classes. The documentation recommends creating your own subclass of that TestCase in your project so you can add project specific helpers.

### Which Django and Python versions does django-test-plus support?

Python 3.11 through 3.15 including the free-threaded 3.14t and 3.15t builds, and Django 4.2 LTS, 5.1, 5.2 LTS, 6.0 and 6.1. The packaging metadata sets requires-python to 3.11 or newer.

### Can I use django-test-plus with pytest?

Yes. There is a pytest fixture, so asking for tp gives you the same helpers, as in a test that asserts tp.reverse('api') equals '/api/'. The fixture is registered through a pytest11 entry point pointing at test_plus.plugin.

### What does assertGoodView check in django-test-plus?

It checks that the view returns a 200 status and that it does so within a query budget, which is what the documentation illustrates as 200 OK in under 50 queries in one line. The underlying helper is assertNumQueriesLessThan.

## Sources

- [License: BSD-3-Clause](https://github.com/revsys/django-test-plus/blob/main/LICENSE)
- [Project website](https://django-test-plus.readthedocs.io)
- [README](https://github.com/revsys/django-test-plus/blob/main/README.md)
- [Releases](https://github.com/revsys/django-test-plus/releases)
- [revsys/django-test-plus on GitHub](https://github.com/revsys/django-test-plus)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/revsys-django-test-plus
