loading...
Cover image for What to test in Django: Models

What to test in Django: Models

alchermd profile image John Alcher Updated on ・6 min read

Introduction

Software testing is the discipline of determining correctness of a software. Automated tests, on the other hand, is the process of writing program that tests a software automatically. There is a whole plethora of discussions around the internet[1] about the upside of writing automated test suites, so I'm not going to bore you with my own opinions on the matter. But if you care about the quality of software you produce, and you already test your programs manually, I can confidently assure that having an automated test suite will increase your productivity and confidence with your codebase.

The Django web framework provides a solid set of testing facilities out of the box adhering to the batteries included philosophy. With these already provided, [2]you can use a collection of tests — a test suite — to solve, or avoid, a number of problems:

  • When you’re writing new code, you can use tests to validate your code works as expected.
  • When you’re refactoring or modifying old code, you can use tests to ensure your changes haven’t affected your application’s behavior unexpectedly.

A Django project contains a bunch of moving parts which are prime candidates to write tests for. This article will focus on Django Models.

Anatomy of a Django Model

Let's start by examining what a Django Model looks like. Using the Sakila DVD Rental Database as a reference (ER diagram[3]), we might represent the actor table with this code:

from django.db import models

# (1)
class Actor(models.Model):                             
    # (2)
    first_name = models.CharField(max_length=255)      
    last_name = models.CharField(max_length=255)
    last_update = models.DateTimeField(auto_now=True)

    # (3)
    film = models.ManyToManyToManyField(               
        "films.models.Film",
        related_name="actors",
        null=True,
        on_delete=models.SET_NULL,
    )

    # (4)
    def __str__(self):                                 
      return f"{self.first_name} {self.last_name}"

This code shows as a few things:

  1. A Django model inherits from the django.db.models.Model class, allowing it to be mapped to a specific database table.
  2. Instances of django.db.models.Fields can then be attached as properties to the model, allowing them to be mapped as table columns. Here we define a Many-to-Many relationship between an Actor and a Film. Note that the null and on_delete kwargs implies that an Actor doesn't necessarily need to have a Film attached to it.
  3. Relationships are an integral part of any RDBMS, and Django allows us to conveniently construct and access these relationships in an Object-Oriented manner.
  4. Finally, models are just Python classes underneath the hood, so we can define custom (or magic) methods for business/presentation/meta logic.

With this information in mind, we can now explore how and what we can test a Django Model.

Testing Model Fields

Writing tests for a Model's fields are probably the easiest and requires the least effort. Basically, we would be asserting that a model does indeed have the expected fields, giving us guarantee that the corresponding table columns exist. Here's what it looks like:

from datetime import datetime
from django.test import TestCase

from .models import Actor

# (1)
class ActorModelTest(TestCase):              
    # (2)
    @classmethod
    def setUpTestData(cls):                                     
        cls.actor = Actor.objects.create(
            first_name="John", 
            last_name="Doe"
        ) 

    # (3)
    def test_it_has_information_fields(self):                   
        self.assertIsInstance(self.actor.first_name, str)
        self.assertIsInstance(self.actor.last_name, str)

    # (4)
    def test_it_has_timestamps(self):                           
        self.assertIsInstance(self.actor.last_update, datetime)

Here's what this code does:

  1. We subclass the Django TestCase class, which in turn inherits from the unittest module's TestCase. This allows us to use assertion methods such as assertTrue() and assertEquals(), as well as some database access facilities such as setUpTestData().
  2. Using the aforementioned setUpTestData() class method, we create a single Actor instance to be used by the rest of the test methods. Read the documentation on why we use this instead of the setUp() method from unittest.TestCase.
  3. We then test that the first and last name fields do exist in the created Actor instance. We do this by running assertions on the specific properties that they are of a specific type, in this case str.
  4. Similarly, we check if the last_update field exists as well, asserting that it is of the datetime.datetime type.

Running your test suite will then confirm that, indeed, the Actor model has the fields you expect!

Testing Model Relationships

Testing for Model Relationships works similarly with Model Fields:

# ...

from films.models import Film

class ActorModelTest(TestCase):
    #  ...

    def it_can_be_attached_to_multiple_films(self):
        # (1)
        films = [Film.objects.create() for _ in range(3)]      
        # (2)
        for film in films:
            film.actors.add(self.actor)                         

        # (3)
        self.assertEquals(len(films), self.actor.films.count()) 
        for film in films:
            # (4)
            self.assertIn(film, self.actor.films)               

The new test method should be self-explaining:

  1. We create a bunch of Film objects to test with.
  2. We then attach the Actor instance we created in the setUpTestData() method for each of the Film's related Actors.
  3. Our Actor instance should have the same amount of films as we created.
  4. And each of the films we created should be in the Actor's set of related films.

We can now be sure that our Models are connected properly.

Intermezzo: What's up with these tests?

A lot of people would say that these methods are testing the framework code and that we didn't accomplish much by writing these tests. I say that they test our integration with the framework (i.e. testing that we use the correct Django's database access features) instead of testing how the framework code works (i.e. testing the internal implementation of an IntegerField). And those are perfectly fine in my book.

At the very least, these types or tests are very easy and fast to write. And momentum is very important in testing, doubly so if you follow TDD. If still in doubt, tests like these are easy to delete down the line.

Considere them a low-risk, medium-return investment on your portfolio of test suites.

Testing Model Methods

Lastly, let's see how one would write a test for custom Model methods:

# ...

class ActorModelTest(TestCase):
    #  ...

    # (1)
    def test_its_string_representation_is_its_first_and_last_name(self): 
        full_name = f"{self.first_name} {self.last_name}"
        # (2)
        self.assertEquals(str(self.actor), full_name)                    
  1. A quick tip: tests are meant for validation, but they are also perfect as added documentation. Don't be afraid to write overly descriptive names for your test methods. One should be able to glance over a testcase and get an overhead view on how the specific module or class works.
  2. Testing our __str__() method. Should be self-explanatory.

We can see that this is a pretty trivial scenario, but logic on custom methods can grow quickly if not kept in check. When more tables and fields are added down the line, we might have the following methods on the Actor class:

  1. A good one is a check_availability(timedelta) method that checks if the Actor can be booked for a certain period of time.
  2. Or a compute_salary(movie) for calculating how much an Actor should be paid for a specific Movie.
  3. Maybe even a retire() method that sets some database fields for the Actor, signifying that the Actor no longer accepts work.

All of these methods require custom logic, and writing tests for these will give us confidence that our code adheres to what we expect it to do.

Conclusion

In this article, we have touched on the following points:

  • What software testing is, and what Django provides in its testing facilities.
  • What a Django Model is made of.
  • How to test a Model's fields.
  • How to test a Model's relationships.
  • How to test a Model's custom methods.

I hope you get something out of this article. May your tests be green and plenty. Stay safe!

Footnotes

Posted on by:

alchermd profile

John Alcher

@alchermd

Writing software for fun and profit.

Discussion

markdown guide
 

I'm on the side of those who think testing the framework is a bad idea. Unless there is a specific business logic that needs more validation like the str representation if it's redefined or a many-to-many field that has the through attribute.

I nonetheless really enjoyed your article, well written. I discovered the setUpTestData that I've never used, but I know on some occasions it would have been handy. I hope to read more advanced Django testing from you in the futur.

 

I really appreciate the feedback. If I may ask: how would you define models in a TDD-manner? In my case, the process that I defined works wonderfully: I write tests on what a Model might look like (fields), run them to failure, and then write the model itself. Property assertion seems weird to me at first, then I realized it's the same thing as testing getters and setters: not that much value themselves, but more of a "cover the bases" thing.

 

Very nice content, it's cool to see some Django tutorial on dev.to :)

 

Very much appreciated, Corentin!