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:
- A Django model inherits from the
django.db.models.Model
class, allowing it to be mapped to a specific database table. - Instances of
django.db.models.Field
s 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 thenull
andon_delete
kwargs implies that an Actor doesn't necessarily need to have a Film attached to it. - Relationships are an integral part of any RDBMS, and Django allows us to conveniently construct and access these relationships in an Object-Oriented manner.
- 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:
- We subclass the Django
TestCase
class, which in turn inherits from theunittest
module's TestCase. This allows us to use assertion methods such asassertTrue()
andassertEquals()
, as well as some database access facilities such assetUpTestData()
. - 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 thesetUp()
method fromunittest.TestCase
. - 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
. - Similarly, we check if the
last_update
field exists as well, asserting that it is of thedatetime.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:
- We create a bunch of Film objects to test with.
- We then attach the Actor instance we created in the
setUpTestData()
method for each of the Film's related Actors. - Our Actor instance should have the same amount of films as we created.
- 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)
- 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.
- 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:
- A good one is a
check_availability(timedelta)
method that checks if the Actor can be booked for a certain period of time. - Or a
compute_salary(movie)
for calculating how much an Actor should be paid for a specific Movie. - 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
- [1] a few links that you might find interesting:
- [2] in verbatim, https://docs.djangoproject.com/en/3.1/topics/testing/
- [3] from https://www.postgresqltutorial.com/postgresql-sample-database/
Top comments (4)
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!