Hello! Welcome to the sixth and final lesson in our module on testing backend applications.
In the last lesson, we learned how to measure test coverage, giving us a clear picture of which parts of our code are exercised by our tests. While coverage tells us what we've tested, it doesn't prescribe a process for how to write those tests and the code they cover. Today, we'll explore a powerful methodology that places testing at the very heart of the development process: Test-Driven Development (TDD).
This lesson will introduce you to a disciplined approach where you write a failing test before you write any production code. We will learn how this practice can lead to cleaner, more robust, and more maintainable software.
By the end of this lesson, you will be able to apply the Test-Driven Development (TDD) cycle—Red, Green, Refactor—to implement a new feature in a Django application.
1. The TDD Cycle: Red, Green, Refactor
Test-Driven Development is built around a short, repetitive cycle. Instead of writing application code and then testing it, you reverse the process.

The cycle consists of three phases:
- RED: Write a small, automated test for a new piece of functionality. This test must fail because you haven't written the application code yet. The failure confirms that your test is working correctly and that the feature doesn't already exist.
- GREEN: Write the simplest possible production code to make the failing test pass. The goal here is not to write perfect code, but to get a passing test as quickly as possible.
- REFACTOR: With the safety net of a passing test, you can now clean up your code. Improve the design, remove duplication, and enhance readability in both your application code and your test code, all while ensuring the tests remain green.
This cycle is repeated for every small increment of functionality, guiding your design one step at a time.
2. The Philosophy and Rules of TDD
Before we jump into a practical example, it's important to understand the "why" behind TDD. It's more than just a testing technique; it's a design discipline.
TDD Full Course (Learn Test Driven Development with Python)
To understand the core principles, watch this introduction from the 'TDD Full Course' by Wes Doyle. It explains the philosophy of TDD and its benefits, such as managing complexity and creating living documentation for your code.
Watch the first 7 minutes of the video. Focus on the benefits of TDD and the explanation of the three fundamental rules.
To practice TDD effectively, it's helpful to adhere to a strict set of rules, often attributed to Robert C. Martin ("Uncle Bob").
TDD Practice: Time API with Django
The article 'TDD Practice: Time API with Django' provides a concise summary of these rules.
Read the section titled 'So, what is TDD?' and pay close attention to the three rules listed under the 'Uncle Bob' heading.
To summarize the three rules:
- You cannot write production code until you have written a failing unit test.
- You cannot write more of a unit test than is sufficient to fail.
- You cannot write more production code than is sufficient to pass the one failing unit test.
Following these rules forces you to think clearly about the requirements for each piece of code before you write it, leading to a more intentional and robust design.
3. TDD in Action: Building a Feature
Now, let's see how this cycle works in practice within a Django project. We will follow a developer building a simple CRUD (Create, Read, Update, Delete) application for tasks.
Step 1: Driving Model Creation with Tests
The first feature is a Task model. Notice how the developer writes tests that force the creation and evolution of the model.
Learn Django TDD - Test Driven Development - CRUD
The video 'Learn Django TDD' by Code With Stein provides an excellent, real-time demonstration of the TDD workflow. We'll start by watching how a test is used to drive the creation of a Django model.
Watch from 01:27 to 07:32. Observe the Red-Green-Refactor cycle in action: Red (01:27 - 03:00): A test test_task_model_exists is written. It fails first with a NameError (the model isn't imported) and then an ImportError (the model file doesn't exist). This is the 'Red' phase. Green (03:00 - 04:30): The minimal Task model is created. The test now fails because migrations haven't been run. After running migrations, the test finally passes. This is the 'Green' phase. Red (04:30 - 05:40): A new test, test_model_has_string_representation, is added to check the model's __str__ method. It fails with an AssertionError. Green (05:40 - 07:32): The __str__ method is implemented, making the test pass. A small refactor is even needed in the test itself to make it correct.
This sequence perfectly illustrates how TDD works for non-HTTP logic. Each failing test gives you a clear, specific error to solve, guiding you to the next small implementation step.
Test your understanding!
Imagine you want to add a completed boolean field to the Task model. The requirement is that this field should default to False when a new task is created. Following TDD, what is the very first test you would write, and what failure would you expect?
Show answer
The first test should verify the default value. You would add a test method like this to your TaskModelTest class:
# In tests.py
def test_new_task_is_not_completed_by_default(self):
# Arrange: Create a new task without specifying 'completed'
task = Task.objects.create(title="A new task")
# Act & Assert: Check the value of the 'completed' field
self.assertFalse(task.completed)
The expected failure would be an AttributeError, because the Task model object has no attribute named completed. This failure is the signal to go and add the completed = models.BooleanField(default=False) field to your Task model in models.py.
Step 2: Driving Views and Templates with Tests
Next, let's see how TDD applies to building a web page. The goal is to create an index page that lists all tasks.
Learn Django TDD - Test Driven Development - CRUD
We'll continue with the same video, now focusing on how tests for an HTTP endpoint guide the creation of the URL, view, and template.
Watch from 07:32 to 13:40. Follow the cycle as the developer builds the index page: Red: A test is written to check for a 200 status code and that task/index.html is used. It fails because no URL or view exists. Green: The index view function, URL pattern, and an empty index.html file are created. Each step resolves a specific failure (URL not found, Template not found) until the test passes. Red: A new test is added to assert that a task's title appears on the page. This fails because the view doesn't pass any tasks to the template, and the template doesn't render them. Green: The view is updated to query for tasks and pass them in the context. The template is updated with a for loop to display them. The test now passes.
This section demonstrates how TDD works for integration tests. The tests don't just check a single function; they test the entire request-response cycle and drive the creation of all the necessary components (URLconfs, views, templates) in a coordinated way.
4. TDD for More Complex Logic: Handling Forms
The TDD cycle is particularly valuable when dealing with more complex logic, such as form submissions, which can have multiple outcomes (valid vs. invalid).
Let's observe how TDD is used to build a "new task" page that handles both successful form submissions and validation errors.
Learn Django TDD - Test Driven Development - CRUD
Finally, let's watch how TDD handles branching logic like form validation. This is where the discipline really shines, ensuring all paths are tested.
Watch from 20:11 to 33:24. This is a longer segment, but it's packed with great examples. Focus on how separate tests are written for: The initial GET request for the form page. Rendering the form in the template (<form>, csrf_token). An invalid POST submission (e.g., missing title), which should re-render the page with error messages. A valid POST submission, which should create a new task in the database and redirect the user.
By writing separate tests for the "happy path" (valid submission) and the "unhappy path" (invalid submission), the developer ensures that the view correctly handles both cases. The tests force the implementation of the if request.method == 'POST', if form.is_valid(), and redirect() logic. This is a prime example of how TDD leads to more robust and complete features.
Conclusion
In this lesson, we've moved from simply measuring tests to using them as a design tool. Test-Driven Development provides a systematic way to build software that is, by its nature, well-tested and robust.
Key Takeaways:
- TDD is a discipline that follows a simple, powerful cycle: Red, Green, Refactor.
- Red: Write a small, failing test that defines the next piece of desired behavior.
- Green: Write the absolute minimum code required to make that test pass.
- Refactor: Clean up and improve the code, confident that your tests will catch any regressions.
- This cycle applies to all aspects of a Django application, from models and business logic to views, forms, and templates.
- TDD naturally leads to high test coverage and provides a safety net that enables confident refactoring and future development.
Preview of the Next Lesson:
This lesson concludes our module on testing. You now have a comprehensive toolkit for writing unit and integration tests, managing test data, measuring coverage, and applying the TDD methodology.
In our next module, "Advanced Database Operations and Performance," we will shift our focus to the database. We'll begin by learning how to move beyond SQLite and configure your Django project to use a production-grade database like PostgreSQL.