How to Test Django Models (with Examples)
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
andrelease_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
anddeleted
. I want to make sure these are both assigned a value ofFalse
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:
gist.github.com/aliceridgway/7c48b84e3a7fa0..
My Model
Below is the code of the model that passes the tests:
gist.github.com/aliceridgway/fca1b492a23292..
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.