PYTHON / DJANGO
Testing a Django application
Write and run Django tests that use a throwaway test database, the test Client, and reverse() to check views, models, and data isolation.
What you will learn
- Write TestCase classes and run them with manage.py test
- Drive views through self.client instead of a real HTTP server
- Use setUpTestData for shared fixtures and rely on per-test rollback
- Pick SimpleTestCase, TestCase, or TransactionTestCase for the right isolation
Understanding Testing a Django application
Django's test runner never touches your development database. Before the first test it creates a separate database named test_<yourdbname> (or an in-memory one for SQLite), applies your migrations to it, and drops it at the end. That is why a broken or missing migration makes tests fail even when runserver works: the test schema comes from the migration files, not from your models.py.
django.test.TestCase wraps every single test method in a database transaction and rolls it back when the method returns. Nothing you create in one test is visible in the next, and no cleanup code is needed. setUpTestData runs once per class inside an outer transaction, so its objects are restored for every test rather than being re-inserted; treat those objects as read-only shared data because mutating them in memory can surprise you.
self.client is a fake browser that calls your URL resolver, middleware stack, view, and template rendering in the same process, with no socket and no server running. It returns the response object your view produced, so you can inspect response.status_code, response.context, and response.content directly. Combine it with reverse('name') so tests describe intent rather than hardcoded paths, and a URL change breaks one urls.py line instead of fifty tests.
import django
from django.conf import settings
settings.configure(
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
INSTALLED_APPS=["django.contrib.contenttypes", "__main__"],
ROOT_URLCONF="__main__",
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
)
django.setup()
from django.db import models
from django.http import HttpResponse
from django.test import TestCase
from django.test.utils import get_runner
from django.urls import path, reverse
class Book(models.Model):
title = models.CharField(max_length=100)
in_stock = models.BooleanField(default=True)
def book_list(request):
titles = Book.objects.filter(in_stock=True).values_list("title", flat=True)
return HttpResponse("\n".join(titles))
urlpatterns = [path("books/", book_list, name="book-list")]
class BookListTests(TestCase):
classmethod
def setUpTestData(cls):
Book.objects.create(title="Dune")
Book.objects.create(title="Ubik", in_stock=False)
def test_isolation(self):
Book.objects.create(title="Valis")
self.assertEqual(Book.objects.count(), 3)
def test_only_in_stock_titles_render(self):
response = self.client.get(reverse("book-list"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Dune")
self.assertNotContains(response, "Ubik")
self.assertEqual(Book.objects.count(), 2)
if __name__ == "__main__":
get_runner(settings)(verbosity=1).run_tests(["__main__"])
A Django test runs against a disposable, migration-built database inside a transaction that is rolled back, and reaches views through an in-process client rather than the network.
Worked examples
SimpleTestCase refuses database access
Shows that the base class you choose decides whether queries are allowed at all.
import django
from django.conf import settings
settings.configure(
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
INSTALLED_APPS=["django.contrib.contenttypes", "django.contrib.auth"],
)
django.setup()
from django.contrib.auth.models import User
from django.test import SimpleTestCase, TestCase
from django.test.utils import get_runner
class NoDatabaseTests(SimpleTestCase):
def test_query_is_blocked(self):
with self.assertRaises(AssertionError) as ctx:
User.objects.count()
print("blocked:", "Database queries" in str(ctx.exception))
class WithDatabaseTests(TestCase):
def test_query_is_allowed(self):
User.objects.create_user("ada")
print("count:", User.objects.count())
if __name__ == "__main__":
get_runner(settings)(verbosity=1).run_tests(["__main__"])
Example explained
Line 1SimpleTestCase installs a guard that raises DatabaseOperationForbidden, a subclass of AssertionError, on any query.
Line 2The guard exists because SimpleTestCase gives no transaction wrapping, so a stray write would leak into other tests.
Line 3WithDatabaseTests subclasses TestCase, so the same manager call works and the created user is rolled back afterwards.
Line 4Test classes run in alphabetical order here, which is why the blocked message appears before the count.
Important notes
Django forces DEBUG=False during tests, so you get the real 404/500 handlers rather than the yellow debug page; use assertContains or response.context to inspect what happened.
transaction.on_commit callbacks never fire under TestCase because the transaction is rolled back; wrap the code in self.captureOnCommitCallbacks(execute=True) or use TransactionTestCase.
Common mistakes
Hardcoding paths like '/books/' instead of reverse('book-list'), so every URL change breaks unrelated tests for the wrong reason.
Forgetting to run makemigrations after a model change: the test database is built from migrations, so tests fail with 'no such column' while the dev server still works.
Assuming objects created in one test method are still there in the next, then writing tests that only pass in a particular order.
Try it yourself
Change, predict, then run
Add a detail view at books/<int:pk>/ that returns 404 for a missing id, then write two tests: one asserting status_code 200 for an existing book and one asserting 404 for pk 999.
Open the Python workspaceCheck your understanding
test_a creates ten Book rows and passes. test_b then asserts Book.objects.count() == 0 and also passes. What makes that possible?
- TestCase runs each test method inside a transaction and rolls it back when the method finishes
- The test runner drops and recreates the test database before every test method
- TestCase deletes all rows from every table in an implicit tearDown
- Objects created in a test are kept in memory and never reach the database
Show answer
TestCase opens a transaction before each test and rolls it back after, so writes are discarded without any deletion work. Recreating the database per method would be correct in effect but is not what happens; migrations run once per test session, and doing that per test would make suites unbearably slow. The rows really are inserted, which is why constraints and triggers still apply during the test.