Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 47

Testing Django Models and Business Logic

Recommended resources

Hello! Welcome back to our module on testing.

In our last lesson, we explored the testing pyramid, establishing a strategic framework for creating a robust and maintainable test suite. We learned that the foundation of this pyramid is made of fast, isolated unit tests. Today, we will move from theory to practice and begin building that foundation.

This lesson focuses on writing your first unit tests for the two most fundamental parts of your application: Django models and standalone business logic functions. By testing these core components in isolation, you can quickly verify your application's fundamental rules and behaviors, catching bugs early and building a reliable codebase.

By the end of this lesson, you will be able to write unit tests for Django models and business logic functions.

1. The Anatomy of a Django Test

Before we write our first test, let's get acquainted with Django's testing framework. Django's testing tools are built on top of Python's standard unittest library and provide a powerful environment for testing web applications.

Visualizing Testing in Django with Python
Caption: Django provides a sophisticated testing framework, illustrated here, to run automated unit and integration tests, ensuring your code works as expected.

Here are the key components:

  • Test Location: By default, Django's test runner discovers tests in any file in your apps whose name begins with test (e.g., tests.py, test_models.py). A common practice is to create a tests/ directory inside your app to organize tests by function (e.g., test_models.py, test_views.py).
  • Test Runner: You execute your tests using the command python manage.py test. This command finds all tests in your project, creates a separate, temporary test database, runs the tests, and then destroys the test database. This ensures your tests are isolated from your development database and from each other.
  • TestCase Class: The most common way to write tests is to create a class that inherits from django.test.TestCase. This class provides a suite of assertion methods (e.g., assertEqual, assertTrue) and sets up a new, empty database for your tests, ensuring a clean slate for every run.

Django Tutorial Part 10: Testing a Django web application

The Mozilla Developer Network (MDN) provides an excellent overview of the structure and execution of Django tests. This will solidify your understanding of the basic setup.

Please read the sections 'What does Django provide for testing?', 'Test structure overview', and 'How to run the tests'. Focus on understanding the role of the TestCase class, the file structure, and the manage.py test command.


2. Writing Unit Tests for Models

Models are the heart of your Django application; they define your data structure and core business logic. Unit testing them is crucial for ensuring data integrity and correct behavior.

But what should you test? You don't need to test that a CharField stores a string—Django already guarantees that. Instead, you should test your own code and design decisions.

Django Tutorial Part 10: Testing a Django web application

Let's revisit the MDN article to understand exactly which aspects of a model we should focus on testing.

Read the sections 'What should you test?' and 'Models'. Pay close attention to the examples of testing field labels (verbose_name), max_length, and custom methods like __str__() and get_absolute_url().

As the article explains, you should test things like:

  • Custom model methods (e.g., was_published_recently()).
  • The string representation of the model (__str__).
  • Field attributes that represent your design, like max_length, default values, and verbose_name.
  • Any custom logic in the save() method.

Practical Example: Testing a Post Model

Let's imagine we have a simple Post model in a blog app:

# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils.text import slugify

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    slug = models.SlugField(max_length=200, unique=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        # If the slug is not set, create it from the title
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

Now, let's write tests for it in blog/tests/test_models.py.

The following video provides a comprehensive walkthrough of setting up test data and writing model tests. It covers creating model instances, using assertions, and the invaluable setUp method for preparing test data efficiently.

Mastering Django Unit Testing: An In-Depth Introduction and Tutorial

This video, 'Mastering Django Unit Testing', provides a practical, step-by-step guide to testing Django models. It clearly demonstrates the concepts we've discussed.

Watch the section from 06:03 to 15:41. Focus on how the setUp method is used to create user and category objects once, which are then used across multiple tests. Also, observe the assertions used to verify the model's string representation and instance type.

Inspired by the video and our Post model, let's write our first test. We'll verify that our custom save() method correctly generates a slug from the title.

# blog/tests/test_models.py
from django.test import TestCase
from django.contrib.auth.models import User
from blog.models import Post

class PostModelTest(TestCase):

    @classmethod
    def setUpTestData(cls):
        # Create a user that will be used by all test methods
        cls.user = User.objects.create_user(username='testuser', password='password123')

    def test_slug_is_generated_on_save(self):
        """Tests that a slug is automatically generated when a post is saved."""
        post = Post.objects.create(
            title="A New Post With A Title",
            content="Some interesting content.",
            author=self.user
        )
        # The save() method is called by create()
        
        self.assertEqual(post.slug, "a-new-post-with-a-title")

    def test_str_representation(self):
        """Tests the __str__ method of the Post model."""
        post = Post.objects.create(
            title="My Test Post",
            content="Content for the test.",
            author=self.user
        )
        self.assertEqual(str(post), "My Test Post")

Dissecting the Test:

  1. setUpTestData: We use this class method to set up data that is needed for multiple tests but is not modified by them. It runs only once per test class, making it efficient. Here, we create a single User object.
  2. test_slug_is_generated_on_save: This method tests a specific piece of business logic.
    • It creates a Post instance. The objects.create() method automatically calls save().
    • It then uses self.assertEqual() to assert that the slug field has been populated with the expected, slugified version of the title.
  3. test_str_representation: This method tests our __str__ override, ensuring it returns the post's title.

To run these tests, you would navigate to your project's root directory and run:
python manage.py test blog

Test your understanding!

Based on the Post model and the tests we've written, write a new test method test_title_max_length inside the PostModelTest class. This test should verify that the max_length attribute of the title field is correctly set to 200.

Hint: You can access a model field's metadata via the _meta attribute, like so: post._meta.get_field('field_name').max_length.

Show answer

Here's how you could write the test:

# Add this method to the PostModelTest class

def test_title_max_length(self):
    """Tests the max_length of the title field."""
    # Get the post object created in another test or create a new one
    post = Post.objects.create(title="A title", content="...", author=self.user)
    max_length = post._meta.get_field('title').max_length
    self.assertEqual(max_length, 200)

3. Writing Unit Tests for Business Logic Functions

Not all logic lives inside a model. You will often write utility or helper functions that encapsulate specific business rules. These are prime candidates for unit testing because they are typically self-contained and have clear inputs and outputs.

For example, let's say our blog needs a function to estimate the reading time of a post. We could create a file blog/utils.py.

# blog/utils.py
import math

def calculate_reading_time(text, words_per_minute=200):
    """
    Calculates the estimated reading time for a given text in minutes.
    """
    if not text:
        return 0
    
    word_count = len(text.split())
    reading_time = math.ceil(word_count / words_per_minute)
    return reading_time

This is a pure Python function—it has no knowledge of Django models or databases. Testing it is straightforward. We can add a new test class to our test_models.py file or create a new test_utils.py file.

# In blog/tests/test_utils.py (or another test file)
from django.test import TestCase
from blog.utils import calculate_reading_time

class UtilsTest(TestCase):

    def test_calculate_reading_time(self):
        """Tests the reading time calculation for various texts."""
        # Test with a typical text
        text_400_words = "word " * 400
        self.assertEqual(calculate_reading_time(text_400_words), 2) # 400 / 200 = 2

        # Test with text that should round up
        text_450_words = "word " * 450
        self.assertEqual(calculate_reading_time(text_450_words), 3) # 450 / 200 = 2.25 -> ceil is 3
        
        # Test edge case: empty text
        self.assertEqual(calculate_reading_time(""), 0)

        # Test edge case: text with less than one minute reading time
        text_50_words = "word " * 50
        self.assertEqual(calculate_reading_time(text_50_words), 1) # 50 / 200 = 0.25 -> ceil is 1

Notice this test doesn't interact with the database at all. It simply calls the function with different inputs and asserts that the returned output is correct. This is the essence of a unit test: verifying a small, isolated piece of logic.


Conclusion

Congratulations! You have taken your first practical steps into automated testing in Django. You've learned how to set up and write unit tests for the foundational layers of your application: models and business logic functions.

Key Takeaways:

  • Tests are organized in files like tests.py and run with python manage.py test.
  • The django.test.TestCase class provides an isolated test environment with a temporary database.
  • The setUpTestData method is efficient for creating shared, unmodified test data.
  • When testing models, focus on your own custom logic and design choices (__str__, custom save methods, field constraints) rather than Django's built-in functionality.
  • Standalone business logic functions should be tested in isolation by providing various inputs and asserting the expected outputs.

Preview of the Next Lesson:
We have successfully built the base of our testing pyramid. In the next lesson, we will move one level up to integration testing. You will learn how to write tests for your API endpoints using Django REST Framework's test client, verifying that different parts of your application—like views, serializers, and the database—work together as intended.