a blog about Django & Web Development

How to Test Django Models (with Examples)

Learn how to test Django models.
Testing Django Models

Today we are looking at how to test Django models. If you’re completely new to testing, then check out my beginner’s guide to testing Django applications.

The application I’m going to test has a model called Movie. My application fetches movies from TheMovieDB API and saves them to the database. I’m not going to test fetching data from the API, just the Movie model.

Deciding what to test

The general rule of testing models is you test your own code and your tests should reflect the requirements of your project.

The question is, how deep do you go?

At one extreme, you could just test your model methods and call it a day. At the other extreme, you could test the type and value of every field.

I’ve decided on an approach where I write down the behaviour that’s most important to the success of my project and test those.

Testing a model

These are the behaviours that I want to test:

  • The api_id is the ID of the movie in TheMovieDB database. This must be unique so I can reliably fetch data about a particular movie.
  • The bare minimum data to create a movie in the database is the api_id, title and release_date. Everything else is optional. I’m going to test that I can create a movie with just those three fields.
  • A slug must be generated for each movie automatically from the title. The slug must be unique, even if there are there are two movies with the same title.
  • I’ve got a column called added which stores the date and time that the movie was added to my database. I want to check that the date is added automatically.
  • I’ve got two booleans: active and deleted . I want to make sure these are both assigned a value of False by default.
  • I want to test the str() method to make sure movies are represented by their title.
  • I have defined a method called get_absolute_url() method which returns the URL of a movie for use in my templates. I want to make sure this returns the expected value.

Set up

Before each test, I’m going to create a movie in the database. This is so I don’t have to repeat Movie.objects.create(...) inside every test.

def setUp(self):
     self.movie = Movie.objects.create(
        title=self.MOVIE_TITLE,
        release_date=self.RELEASE_DATE,
        api_id=1
     )

Writing the tests

Test uniqueness

I’m going to test that the database doesn’t allow two movies with the same api_id. I’m going to do this by attempting to create a second movie with the same api_id and testing that an IntegrityError is raised.

def test_unique_api_id_is_enforced(self):
    """ Test that two movies with same api_id are not allowed."""
    with self.assertRaises(IntegrityError):
        Movie.objects.create(
            title="another movie",
            release_date=self.RELEASE_DATE,
            api_id=1
        )

Test that the slug is automatically applied

If I create a movie with a title, api_id and release_date, I want to check that the slug is automatically applied.

I calculate the expected value using the slugify method. This means if I change MOVIE_TITLE, I don’t have to update the test.

The actual value is stored in movie.slug.

def test_slug_value(self):
    """
    Test that the slug is automatically added to the movie on creation.
    """
    expected = slugify(self.movie.title)
    actual = self.movie.slug
    self.assertEqual(expected, actual)

Test the slug is unique

I don’t need titles to be unique but I do need slugs to be unique as I want to use them in URLs. An IntegrityError isn’t desirable here. I’d rather that my model provides a movie with a non-unique title a unique slug.

For this, I’m going to create a second movie of the same title and test that their slugs are not the same.

def test_slug_value_for_duplicate_title(self):
    """
    Test that two movies with identical titles don't get assigned the same slug.
    """
    movie2 = Movie.objects.create(
        title=self.movie.title,
        release_date=self.RELEASE_DATE,
        api_id=99
    )

    self.assertNotEqual(self.movie.slug, movie2.slug)

Test the added date is added automatically

I want to keep a record of when each of my movies was added to the database but don’t want to have to enter it manually. When a movie is created with a title, release_date and api_id, I want to make sure the added datetime is applied automatically.

To do this, I will assert that movie.added is of the datetime type.

def test_added_date_automatically(self):
    """ Test that the date is automatically saved on creation"""
    self.assertTrue(type(self.movie.added), datetime)

Test booleans are set to False by default

Eventually, I’m going to add a feature where administrators can mark movies as inactive and customers can only book tickets for active movies. I also want the ability to soft-delete movies where it appears deleted from the front-end but still appears in the database.

I want both active and deleted to be False by default (without specifying on object creation).

def test_active_false_by_default(self):
    """ Test that our booleans are set to false by default"""
    self.assertTrue(type(self.movie.active) == bool)
    self.assertFalse(self.movie.active)

def test_deleted_false_by_default(self):
    """ Test that our booleans are set to false by default"""
    self.assertTrue(type(self.movie.deleted) == bool)
    self.assertFalse(self.movie.deleted)

Test the str method

We should test any method that is defined on our model, even simple ones like __str__()

def test_str(self):
    """ Test the __str__ method"""
    expected = "test movie"
    actual = str(self.movie)

    self.assertEqual(expected, actual)

Test the get_absolute_url method

Finally, we need to test get_absolute_url, the method that will return the URL for any movie.

The expected URL will be along the lines of localhost:8000/movies/test-movie in development and https://some-domain/movies/test-movie in production. For this reason I don’t want to hardcode the URL into the test. I have used the reverse method instead.

def test_get_absolute_url(self):
        """ Test that get_absolute_url returns the expected URL"""

        expected = reverse("movie_detail", kwargs={"slug": self.movie.slug})
        actual = self.movie.get_absolute_url()

        self.assertEqual(expected, actual)

Tests in full

Here is the full code for my model tests:

My Model

Below is the code of the model that passes the tests:

Conclusion

We have been through how to test your Django models. The project requirements should guide what you test. In the example above, I chose to test that uniqueness constraints were being enforced and tested that fields like the slug and the added date get assigned the correct value automatically.

In order to achieve a high coverage score for your unit tests, you should test all methods defined on your model, including any special methods like __str__.

While writing tests are time consuming, they will save us time in the long run. Writing tests also helps you understand your code and can also serve as a form of documentation. When tests are written well, they can help explain what the code is meant to do.

Related Posts