Testing: Unit Tests
Every lesson up to this point has asked you to trust that a piece of code behaves the way it's
described. Tests are how you stop trusting and start checking — a small script that creates
some data, runs your code against it, and asserts the result is what you expect. Django doesn't ask you
to learn a separate testing tool for this: it builds directly on Python's own unittest
module, and adds a few Django-specific conveniences on top that make testing models, views, and forms
far less painful than testing them by hand.
Django's TestCase, Not Plain unittest.TestCase
You could write TaskFlow tests using Python's plain unittest.TestCase directly, and
technically they'd run. But Django gives you django.test.TestCase instead, and it's worth
understanding exactly what that swap buys you: every test method runs inside its own database
transaction, and that transaction is automatically rolled back the moment the test
finishes — whether it passed or failed. Any Task or Project a test created
never actually lands permanently in the database.
That matters for two very practical reasons. First, tests never leave leftover rows behind for the
next test run to trip over. Second, tests can't interfere with each other — one test creating
a Task titled "Test task" can't accidentally cause a different, unrelated
test to see it. Each test method starts from a clean slate.
A First Test: Testing the Task Model's __str__()
Back in Custom Managers & Meta Options, TaskFlow's
Task model was given a __str__() method that returns the task's title instead
of Django's default, unhelpful object representation. That's a small piece of behavior, but it's
exactly the kind of thing worth locking down with a test — if someone changes __str__()
later without realizing what depends on it, the test catches the regression immediately:
# taskflow/tests.py
from django.test import TestCase
from .models import Task
class TaskModelTests(TestCase):
def test_str_representation(self):
task = Task.objects.create(title='Test task')
self.assertEqual(str(task), 'Test task')
Every test method's name must start with test_ — that's how Django's test runner finds
it and knows to run it. self.assertEqual() is one of many assertion methods inherited
from unittest.TestCase; it fails the test loudly if the two values it's given don't
match, and passes silently if they do.
Testing a Custom Manager Method
The same lesson also defined Task.objects.incomplete(), a custom manager method that
filters out completed tasks. That filter condition is exactly the kind of logic worth verifying
directly, rather than just trusting it reads correctly:
class TaskManagerTests(TestCase):
def test_incomplete_excludes_completed_tasks(self):
Task.objects.create(title='Write report', completed=False)
Task.objects.create(title='Send invoice', completed=True)
open_tasks = Task.objects.incomplete()
self.assertEqual(open_tasks.count(), 1)
self.assertEqual(open_tasks.first().title, 'Write report')
This test creates one completed and one incomplete task, then checks two things at once: that
incomplete() returns exactly one task, and that it's the right one. Both
Task rows disappear automatically once the test method ends, courtesy of the transaction
rollback described above.
Sharing Setup Across Tests with setUp()
Real tests often need the same starting data — a Project to attach tasks to, a
User to act as its owner — repeated across several test methods in the same class.
Rather than recreating that data by hand inside every single method, Django's TestCase
(inherited from unittest.TestCase) calls a method named setUp() automatically
before every test method runs:
from django.contrib.auth.models import User
from .models import Project, Task
class ProjectTaskTests(TestCase):
def setUp(self):
self.owner = User.objects.create_user(username='sonu', password='testpass123')
self.project = Project.objects.create(
name='Website Redesign', owner=self.owner
)
def test_new_project_has_no_tasks(self):
self.assertEqual(self.project.tasks.count(), 0)
def test_task_links_back_to_its_project(self):
task = Task.objects.create(title='Design homepage', project=self.project)
self.assertEqual(task.project, self.project)
Notice neither test method creates the User or Project itself — both rely
on self.owner and self.project, set up fresh by setUp() right
before each one runs. Because of the transaction rollback, the second test doesn't see any task left
behind by the first — setUp() genuinely runs again, from scratch, for every method.
Running the Test Suite
With tests written, Django's own management command discovers and runs them:
python manage.py test
This walks every app in the project looking for a tests.py file (or a tests/
package), collects every class inheriting from TestCase, and runs every method starting
with test_. Django even spins up a completely separate test database for
the run and destroys it afterward, so none of this touches real data — not even outside the
per-method transaction rollback.
A Note on factory_boy
Manually calling Task.objects.create(...) with every field spelled out gets tedious fast
once a model has many fields, or once tests need dozens of realistic-looking objects. A popular
third-party package called factory_boy addresses this by letting you define a
"factory" for a model once, with sensible defaults for every field, and then generate as many
test objects as you need with a single line — optionally overriding just the fields a specific test
cares about. It's not something you need to master to write solid Django tests, but it's worth knowing
it exists once .objects.create() starts feeling repetitive across a growing test suite.