Horace FAYOMI
Posted on December 31, 2022
One of the best practices in software engineering is to write various kinds of tests after or before we write code, according to the method we use for development like TDD (Test Driven Development). In this tutorial, we will learn the basics of the testing with Django.
This tutorial is the fourth part of a tutorial-course in which you'll learn and understand the basics of Django by building a clone of the popular website Netflix.
First, what to test?
Normally, it's important to test all aspects of our code,
that means we do not test any libraries or functionality provided as part of Python or Django. We should trust our framework (Django) and other libraries.
However, in the real world, we can't test all our code most of the time, but we need to test core features and tricky code.
In this course, we will test the following features:
- movies listing and filtering
- registration
- login.
There are several kinds of tests but in this tutorial, we will focus on integration tests (which consist of testint the entire application, all parts together).
Testing movie listing and filtering
Let's create the test class.
- Open
netflix/tests.py
file (it has been created by default with the app) and add this code:
# netflix/tests.py
from HTTP import HTTPStatus
from copy import deepcopy
from django.contrib.auth.models import User
from Django.test import TestCase
from django.contrib import auth
from netflix.models import Category, Movie
class IndexTests(TestCase):
def setUp(self):
self.category = Category.objects.create(name="Action")
self.spider_man_movie = Movie.objects.create(
name="Spider man",
category=self.category
)
self.avatar_movie = Movie.objects.create(
name="Avatar",
category=self.category
)
def test_index_render_all_movies(self):
pass
def test_index_filter_movies(self):
pass
Explanation:
- The test class is
IndexTests
which inherits fromdjango.test.TestCase
. - It contains 2 tests cases:
test_index_render_all_movies
to test that index route (/
) renders all movies andtest_index_filter_movies
to test that the filter feature works well.
Note: each test is a function of the class with a name starting with test_
.
- It also contains a method
setUp
which is a default method defined by Django test framework to set up the testing environment before each test case. It runs before each test case. Inside that method, we have created a category instance as an attribute to IndexTests class. We have also created two movies instances for that category. We have created them because we need to have at least one category and some movies that we can display to test the listing feature. That is why it's calledSetup
. We should put in it all data and actions required to perform the test.
Note: each test is isolated; that means, to run the test Django will follow these steps:
1- run IndexTests.setUp()
to create the testing environment if there is any, and behind the scenes, Django creates a temporary testing database to store any potential model data.
2- run IndexTests.test_index_render_all_movies()
3- clears the temporary database content so the next test will be executed in a new environment.
4- run IndexTests.test_index_filter_movies()
.
That is why we say that each test case is isolated from others. The changes made in a given test won't affect others and the setup is executed for each test case.
test_index_render_all_movies
Modify the test_index_render_all_movies
method like this:
def test_index_render_all_movies(self):
response = self.client.get("/")
# make sure index displays the two movies
self.assertContains(response, self.spider_man_movie.name)
self.assertContains(response, self.avatar_movie.name)
Explanation:
- we just simulate a
GET
request to/
(index) with the lineresponse = self.client.get("/")
- we assert that the response (the index HTML page) contains the spiderman movie name with
self.assertContains(response, self.spider_man_movie.name)
- we do the same for the avatar movie
test_index_filter_movies
Modify the test_index_filter_movies
method like this:
def test_index_filter_movies(self):
# make sure only `Avatar` movie is rendered when the search term is `ava`
# This also asserts that the search is case insensitive as the real name
# is `Avatar` with an upper `A` and we search `ava`.
response = self.client.post(
"/",
data={"search_text": "avat"}
)
# make sure index displays `Avatar` movie
self.assertContains(response, self.avatar_movie.name)
# Make sure index doesn't display `Spider man` movie
self.assertNotContains(response, self.spider_man_movie.name)
Explanation:
- We simulate the POST on the search form with the search text "avat". Because there is only avatar movie whose names contain
avat
in a case insensitive, then it should be the only movie displayed. - we then assert that the response contains
Avatar
movie but not spider man movie. That means our filter works well.
Testing registration
- Create the test class:
Always inside
netflix/forms.py
file add the following, right afterIndexTests
class:
# netflix/forms.py
TEST_DATA = {
"firstname": "John",
"lastname": "Joe",
"email": "johnjoe@gmail.com",
"password": "NetflixClone2022",
"password_conf": "NetflixClone2022"
}
...
...
class RegisterTests(TestCase):
def test_get_registration_page(self):
pass
def test_registration_with_valid_data(self):
pass
def test_registration_with_empty_fields(self):
pass
def test_registration_with_wrong_password_confirmation(self):
pass
Note: TEST_DATA
is a dictionary that contains good registration information.
Also, we don't need a setup method which is why it's not present.
test_get_registration_page
Here, we test that user that will retrieve the registration page. Add this code:
def test_get_registration_page(self):
# We call the `register` route using `GET`
response = self.client.get("/register")
# We assert that no error is returned
self.assertEqual(response.status_code, HTTPStatus.OK)
# We assert that the returned page contains the button for registration
# form
self.assertContains(
response,
'<button type="submit">Register</button>',
html=True
)
Explanation: We call the registration page using GET
on /register
route and we make sure the response (HTML) contains the register button.
test_registration_with_valid_data
Here, We make sure that we can register with valid data. Add this code:
def test_registration_with_valid_data(self):
# We make sure that no user exists in the database before the registration
self.assertEqual(User.objects.count(), 0)
# We call the `register` route using `POST` to simulate form submission
# with the good data in the setup
self.client.post("/register", data=TEST_DATA)
# We make sure that there is 1 user created in the database after the registration
self.assertEqual(User.objects.count(), 1)
# We make sure that newly created user data are the same as we used during registration
new_user = User.objects.first()
self.assertEqual(new_user.first_name, TEST_DATA['firstname'])
self.assertEqual(new_user.last_name, TEST_DATA['lastname'])
self.assertEqual(new_user.email, TEST_DATA['email'])
Explanation: We first make sure that no user exists in the database at the beginning. Secondly, we simulate the registration form POST with some valid data. Then we make sure that a user instance is created with the data used during the registration form submission.
test_registration_with_empty_fields
def test_registration_with_empty_fields(self):
# We make sure that no user exists in the database before the registration
self.assertEqual(User.objects.count(), 0)
# We call the `register` route using `POST` to simulate form submission
# with empty fields data
response = self.client.post(
"/register",
data={
"firstname": "",
"lastname": "",
"email": "",
"password": "",
"password_conf": ""
}
)
# We make sure that there no user in the database after the registration
# failure. That means no user has been created
self.assertEqual(User.objects.count(), 0)
# Make sure the required message is displayed
self.assertContains(response, 'This field is required')
Explanation: We try to submit the registration form with empty fields and we assert that the response fails and that no user is created.
test_registration_with_wrong_password_confirmation
def test_registration_with_wrong_password_confirmation(self):
# We make sure that there is no user in the database before the registration
self.assertEqual(User.objects.count(), 0)
# We call the `register` route using `POST` to simulate form submission
# with wrong password confirmation good data in the setup
# This time, to create the invalid dict, we create a copy of the good
# data of the setup first.
bad_data = deepcopy(TEST_DATA)
bad_data['password_conf'] = "Wrong Password Confirmation"
response = self.client.post(
"/register",
data=bad_data
)
# We make sure that there is no user in the database after the registration
# failure. That means no user has been created
self.assertEqual(User.objects.count(), 0)
# Make sure the wrong confirmation is displayed
self.assertContains(response, 'wrong confirmation')
Explanation: We try to submit the registration form with a password confirmation value different that the password value. We then make sure an error is raised and no user is created.
Testing login
- Create the test class:
Always inside
netflix/forms.py
file adds the following, right afterRegisterTests
class:
class LoginTests(TestCase):
def setUp(self):
self.user = User.objects.create(username="johnjoe@gmail.com")
self.user_password = "NetflixPassword2022"
self.user.set_password(self.user_password)
self.user.save()
def test_login_with_invalid_credentials(self):
pass
def test_login_with_good_credentials(self):
pass
Explanation:
- We have added a setup because, as we want to just test the login, we need some existing users. Thus we have created a user and set a password
test_login_with_invalid_credentials
Add the following code:
def test_login_with_invalid_credentials(self):
self.assertFalse(auth.get_user(self.client).is_authenticated)
response = self.client.post(
"/login",
data={"email": self.user.username, "password": "Wrong password"}
)
self.assertContains(response, 'Invalid credentials.')
self.assertFalse(auth.get_user(self.client).is_authenticated)
Explanation:
- We make sure that initially the user was not authenticated with
self.assertFalse(auth.get_user(self.client).is_authenticated)
- We try to POST the login form using the wrong password and then make sure that the response contains the error
Invalid credentials.
- We assert that the user is still not authenticated.
test_login_with_invalid_credentials
Add the following code:
def test_login_with_good_credentials(self):
self.assertFalse(auth.get_user(self.client).is_authenticated)
self.client.post(
"/login",
data={"email": self.user.username, "password": self.user_password}
)
self.assertTrue(auth.get_user(self.client).is_authenticated)
Explanation:
- We make sure that initially the user was not authenticated with
self.assertFalse(auth.get_user(self.client).is_authenticated)
- We try to POST the login form using a good password.
- We assert that the user is this time authenticated.
Finally run the test
Just run on the project shell
python manage.py test
The output should looks like this:
Congratulations 🎉. We have tested the main features of our Django project.
Also, remember that the final source code is available here:
https://github.com/fayomihorace/django-netflix-clone
In the next part, we will learn how to deploy your Django application.
Posted on December 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.