To unit-test a Django form, instantiate the form with a data dictionary and assert on form.is_valid() and form.errors; to unit-test a view, use Django's built-in test Client (self.client.get() and self.client.post()) and assert on response.status_code, the template that rendered, and any redirect. Django ships with a test runner built on Python's unittest, and in 2026 most teams pair it with pytest-django for terser, fixture-driven tests.
This guide covers both approaches for Django 5.x: the built-in django.test.TestCase / Client and the modern pytest-django workflow, with working examples for forms, views, authentication, fixtures, and coverage.
Key takeaways
- Test a form by feeding it a data dict and asserting
form.is_valid()(and inspectingform.errorsfor invalid input). - Test a view with the test
Client: assertstatus_code,assertTemplateUsed,assertContains, andassertRedirects. - Use
setUpTestData()for read-only fixtures shared across a test class — it runs once per class and is far faster thansetUp(). self.client.force_login(user)authenticates a user without going through the login view or password hashing.- Django's
TestCaseand pytest-django test the same things; pytest adds fixtures,@pytest.mark.parametrize, and a lighterassertsyntax. - Measure effectiveness with
coverage/pytest-cov, generate data withfactory_boy, and speed runs up withmanage.py test --parallel.
How do you unit test a Django form?
A Django form is just a class that validates a dictionary of data, so it is the easiest thing to unit test: build the form with sample input and check whether it validates. Suppose you have a simple ModelForm. (For a refresher, see Django forms basics.)
# accounts/forms.py
from django import forms
from django.contrib.auth import get_user_model
User = get_user_model()
class UserForm(forms.ModelForm):
class Meta:
model = User
fields = ("email", "first_name", "phone")Now write the test. Every test method name must start with test so the runner discovers it. Use assertTrue(form.is_valid()) for good data, and check form.errors for bad data:
# accounts/tests/test_forms.py
from django.test import TestCase
from accounts.forms import UserForm
class UserFormTests(TestCase):
def test_valid_data(self):
form = UserForm(data={
"email": "user@example.com",
"first_name": "Ada",
"phone": "12345678",
})
self.assertTrue(form.is_valid())
def test_missing_email_is_invalid(self):
form = UserForm(data={
"email": "",
"first_name": "Ada",
"phone": "12345678",
})
self.assertFalse(form.is_valid())
self.assertIn("email", form.errors)
self.assertEqual(form.errors["email"], ["This field is required."])Because plain form validation never touches the database, you can subclass SimpleTestCase instead of TestCase here to skip database setup and run even faster. More on choosing a base class below.
How do you test a Django view with the test Client?
Django's test Client acts like a dummy browser: it issues GET/POST requests against your URLconf and returns the response object without running a real server. Assert on the status code, the template used, the rendered content, and redirects.
Two helpers keep view tests fast and clean:
setUpTestData(cls)creates objects once per test class inside a transaction, so every test method reuses them — much faster than recreating data insetUp().self.client.force_login(user)logs a user in without going through the login form or hashing a password.
# accounts/tests/test_views.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
User = get_user_model()
class UserViewTests(TestCase):
@classmethod
def setUpTestData(cls):
# Runs once for the whole class — shared, read-only fixtures.
cls.user = User.objects.create_user(
email="user@example.com",
password="s3cret-pass",
first_name="Ada",
)
def test_profile_requires_login(self):
# Anonymous users get redirected to the login page.
response = self.client.get(reverse("profile"))
self.assertRedirects(response, "/accounts/login/?next=/profile/")
def test_profile_renders_for_logged_in_user(self):
self.client.force_login(self.user)
response = self.client.get(reverse("profile"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "accounts/profile.html")
self.assertContains(response, "Ada")
def test_create_user_via_post(self):
self.client.force_login(self.user)
before = User.objects.count()
response = self.client.post(
reverse("user-create"),
data={
"email": "new@example.com",
"first_name": "Grace",
"phone": "87654321",
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(User.objects.count(), before + 1)The core view assertions: assertEqual(response.status_code, 200) checks the HTTP status, assertTemplateUsed confirms the right template rendered, assertContains checks the response body (and status) for a string, and assertRedirects verifies both the redirect and the final URL. Note that response.content is bytes, so compare against bytes or call .decode() first — a classic gotcha when checking JSON responses.
Which Django test base class should you use?
Django gives you three base classes that differ mainly in database access and speed:
| Base class | Database access | Rolls back after each test | Use it for |
|---|---|---|---|
SimpleTestCase |
No queries allowed | n/a | Forms (validation only), template tags, utils, request factory |
TestCase |
Yes | Yes — fast transaction rollback | The default for most form / view / model tests |
TransactionTestCase |
Yes | No — truncates tables instead | Testing transaction.atomic(), select_for_update, or commit behaviour |
Reach for TestCase by default; drop to SimpleTestCase only when you are sure no query runs, and escalate to TransactionTestCase only when you must test real transaction semantics (it is noticeably slower).
TestCase vs pytest-django: which should you use?
Both run the same kinds of assertions against forms and views; the difference is ergonomics. Django's TestCase is built in and zero-dependency. pytest-django layers pytest's fixtures, plain assert statements, and parametrization on top of Django's test machinery.
| Aspect | Django TestCase |
pytest-django |
|---|---|---|
| Install | Built in | pip install pytest-django (+ a pytest.ini) |
| Test data setup | setUp / setUpTestData |
Function/class fixtures, @pytest.fixture |
| Assertions | self.assertEqual, self.assertTrue, ... |
plain assert x == y |
| DB access | Automatic in TestCase |
Opt-in via @pytest.mark.django_db or the db fixture |
| Parametrize | Manual loops / subTest |
@pytest.mark.parametrize |
| Test client | self.client |
the client fixture |
| Parallel runs | manage.py test --parallel |
pytest-xdist (-n auto) |
| Ecosystem | Standard-library style | Large pytest plugin ecosystem |
There is no wrong choice. New projects in 2026 often start with pytest-django for the lighter syntax; existing Django suites run fine on the built-in runner. You can even run unittest-style TestCase classes under pytest, so migration can be gradual.
How do you write the same tests with pytest-django?
With pytest-django you skip the class boilerplate. Use the client fixture for the test client and mark any test that touches the database with @pytest.mark.django_db. Plain assert replaces the self.assert* helpers, and @pytest.mark.parametrize covers many inputs in one function:
# accounts/tests/test_pytest_style.py
import pytest
from django.urls import reverse
from accounts.forms import UserForm
@pytest.mark.parametrize("email,expected", [
("user@example.com", True),
("", False),
("not-an-email", False),
])
def test_user_form_validation(email, expected):
form = UserForm(data={"email": email, "first_name": "Ada", "phone": "12345678"})
assert form.is_valid() is expected
@pytest.mark.django_db
def test_profile_view(client, django_user_model):
user = django_user_model.objects.create_user(
email="user@example.com", password="s3cret-pass", first_name="Ada",
)
client.force_login(user)
response = client.get(reverse("profile"))
assert response.status_code == 200
assert "Ada" in response.content.decode()A minimal pytest.ini (or a [tool.pytest.ini_options] table in pyproject.toml) points pytest at your settings:
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = tests.py test_*.py *_tests.pyGenerating test data with factory_boy
Hand-writing Model.objects.create(...) for every test gets tedious. factory_boy defines reusable factories that build valid model instances with sensible defaults, keeping tests short and resilient to model changes. See factory_boy as an alternative to fixtures for a deeper walkthrough.
# accounts/tests/factories.py
import factory
from django.contrib.auth import get_user_model
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = get_user_model()
email = factory.Sequence(lambda n: f"user{n}@example.com")
first_name = "Ada"
phone = "12345678"
# usage inside a test: user = UserFactory()How do you run tests and measure coverage?
Run the built-in suite with the management command; pytest projects use the pytest command. Speed large suites up by spreading tests across CPU cores.
# Django's built-in runner
python manage.py test # run every test
python manage.py test accounts # only the accounts app
python manage.py test --parallel # use multiple CPU cores
python manage.py test --keepdb # reuse the test DB between runs
# pytest-django
pytest # run every test
pytest -k user_form # run tests matching an expression
pytest -n auto # parallel via pytest-xdistCoverage measures the percentage of your code exercised by tests, so you can spot untested branches. Use coverage with the built-in runner, or pytest-cov with pytest:
# With Django's runner
pip install coverage
coverage run --source="." manage.py test
coverage report -m # show missing lines in the terminal
coverage html # browsable HTML report in htmlcov/
# With pytest
pip install pytest-cov
pytest --cov=accounts --cov-report=term-missingHow do you run Django tests in CI/CD?
Tests pay off most when they run automatically on every push. We have wired Django suites into several pipelines — see our guides on running Django tests with self-hosted GitLab CI and Docker and setting up Travis CI for a Django project. For browser-level checks beyond unit tests, acceptance testing with Robot Framework complements your unit suite.
A practical layering: fast unit tests for forms, views, and models on every commit, then a smaller set of acceptance/integration tests for your critical user journeys.
Need a dependable test suite, or higher coverage on an existing Django codebase? Our software testing & QA services and Django development team help teams ship with confidence — from the first TestCase to coverage gates wired into CI.
Frequently Asked Questions
How do you test a Django form?
Instantiate the form with a data dictionary — form = MyForm(data={...}) — then call form.is_valid(). Assert True for good input and False for bad input, and inspect form.errors (a dict keyed by field name) to confirm the right validation message fired. Because plain form validation does not hit the database, these tests can subclass SimpleTestCase and run very fast.
How do you test a Django view?
Use Django's test Client, available as self.client inside a TestCase. Call self.client.get(url) or self.client.post(url, data) and assert on the result: response.status_code, assertTemplateUsed(response, "name.html"), assertContains(response, "text"), and assertRedirects(response, "/target/"). Use self.client.force_login(user) to test views that require authentication.
What is the difference between setUp and setUpTestData?
setUp() runs before every test method, so any objects it creates are rebuilt for each test. setUpTestData() is a classmethod that runs once per test class inside a transaction, and the data is rolled back at the end — making it much faster for read-only fixtures shared across tests. Use setUpTestData for data you do not mutate, and setUp only when each test needs a fresh copy.
Should I use Django TestCase or pytest-django?
Both test the same things. Django's TestCase is built in and needs no extra packages. pytest-django adds fixtures, plain assert statements, @pytest.mark.parametrize, and the pytest plugin ecosystem. Many 2026 projects prefer pytest-django for its terser syntax, but the built-in runner is perfectly capable. You can run existing TestCase classes under pytest, so you can migrate gradually.
How do you test a view that requires login?
Create a user in setUpTestData (or with a factory), then call self.client.force_login(user) before the request — it sets the session without going through the login form or password hashing. In pytest-django, use the client and django_user_model fixtures and call client.force_login(user). To verify an anonymous user is blocked, request the URL without logging in and assert a redirect to the login page with assertRedirects.
How do you measure test coverage in Django?
Install coverage and run coverage run --source="." manage.py test, then coverage report -m for a terminal summary with missing line numbers, or coverage html for a browsable report. With pytest, install pytest-cov and run pytest --cov=yourapp --cov-report=term-missing. Coverage shows the percentage of code executed by tests; aim to cover critical branches rather than chasing 100%.