Unit Tests in Django: What you should know

- Andrés Cruz

En español

Este material forma parte de mi curso y libro completo; puedes adquirirlos desde el apartado de libros y/o cursos Curso y libro desarrollo web con Django 5 y Python 3 + integración con Vue 3, Bootstrap y Alpine.js.

At the moment, we have done some testing methods and there are two factors that we have to keep in mind, if we make requests using Client(), for example:

client = Client()
response = client.get("/")

Or instantiating models or other classes in the test:

Comment.objects.create(text='text 1')

The connections are made in the context of the application under development according to what we have configured in the test:

import unittest.TestCase

Or in memory:

from django.test import TestCase

This is easily probable if we do some tests; if from a test, you try to make a connection to the database using the client or an instance of a model:

import unittest
from django.test import Client

from .models import Comment
class ContactTests(unittest.TestCase):
    def test_index(self):
        print(Comment.objects.all())
        client = Client()
        response = client.get("/")
        print(response)
        ***

And we execute:

$ python manage.py test

We will see that it returns a list with the data that we have in the development database, but, if we configure it with Django's TestCase:

from django.test import Client, TestCase

from .models import Comment
class ContactTests(TestCase):
    def test_index(self):
        print(Comment.objects.all())
        client = Client()
        response = client.get("/")
        print(response)
        ***

We will see that it returns a result empty of comments, even though we have comments in the development database.

In both cases, to obtain at least the database structure, the one we have configured is used:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

To understand how testing works, let's configure the database for the testing environment:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
        'TEST': {
            'NAME': BASE_DIR / 'db.sqlite3',
         },
    }
}

Before executing, is recommended to make a copy of the SQLite database (<your-project>\db.sqlite3) so that you can restore it later, since it will be deleted at the end of the test, finally, we execute:

$ python manage.py test

Regardless of which TestCase class you are using, since it is the same database that is being used for development, it will ask you if you really want to delete it:

Creating test database for alias 'default'...
Destroying old test database for alias 'default'...
Type 'yes' if you would like to try deleting the test database '/Users/andrescruz/Desktop/proyects/django/curso/django-base/db.sqlite3', or 'no' to cancel: 

We say “yes”, since we have the copy:

yes

And we will see that the database is destroyed (if you try to enter the application http://127.0.0.1:8000, you will see that it returns an error that it cannot find the table for the session, which is used internally by Django before resolving the requests implemented by us in the views):

OK
Destroying test database for alias 'default'...

As a result of:

print(Comment.objects.all())

We will see that, although we have data in the development database, the list appears empty (remember, only if we use from django.test import TestCase, which is the one we should always use):

<QuerySet []>

Tests that require a database will not use the "real" (production or development) database of the project. Separate, blank databases are created for testing.

From now on, to do the tests, you must use:

from django.test import TestCase

And not the other one, which is provided by Python:

import unittest.TestCase

Since, the class provided by Django (from django.test import TestCase) does not confirm (commit) the data that we are manipulating in the unit tests and by default, it does not use the records stored in the database, unlike the package Python (import unittest.TestCase) that confirms the operations.

In summary, if you make connections from django.test import TestCase, the context of the configured application is used (which in our case would be the development environment), but if you use import unittest.TestCase, a separate environment provided by Django is used for testing.

With this, we can know the essence of tests, it allows us to create our controlled environment and it is the same every time we run the tests, since, the following steps happen:

  1. The environment is built
  2. We as a developer, create the test data and other instances to be used, for that, we can use the method setUp():
from django.test import Client, TestCase
class ContactTests(TestCase):

    def setUp(self):
        Comment.objects.create(text='text 1')
        Comment.objects.create(text='text 2')
        self.comments = Comment.objects.all()

        self.client = Client()
  1. Create the tests (the rest of the methods in the class).
  2. Django automatically eliminates the previous environment, therefore, for the example that we defined in the previous example, that we created 2 comments, they are automatically destroyed at the end of the test, which is excellent since we can always have an ideal environment, totally controlled by us (including the number of records and their structure), which could not be the case if the environment were not destroyed).

With what has been discussed in this section, you can have a good idea of how tests work and take advantage of this knowledge to create custom unit tests.

Comments Crud

In this section, we will see how to carry out tests for the CRUD of the comments, the developments carried out in this section can be taken as experiments that will help us understand Django tests based on what we experienced in the previous section.

Index

Since we know that, at the time of executing the tests, we cannot have access to the data that we have defined in the development mode, if not, we must create it ourselves when executing the test in the test environment, we are going to configure the initial data as follows:

comments/tests.py

# import unittest
from django.test import Client, TestCase

from .models import Comment
class ContactTests(TestCase):

    def setUp(self):
        Comment.objects.create(text='text 1')
        Comment.objects.create(text='text 2')
        self.comments = Comment.objects.all()
        print(self.comments)
        self.client = Client()

As we mentioned previously, the setUp() method is used to initialize data, for our test it would be the client and the test data that we can use throughout ALL the tests for the comments CRUD, it is important to note that we have a pagination two levels:

comments/views.py

def index(request):
    comments = Comment.objects.all()
    paginator = Paginator(comments,2)
    ***

And that is why we only create 2 comments, since, apart from checking the status code, it is essential that we check the returned content, which in this example, would be the paginated comments listed in the table:

comments/tests.py

class ContactTests(TestCase):
    def test_index(self):
        response = self.client.get("/tasks/")
        self.assertEqual(response.status_code, 200)
        self.assertContains(response,self.comments[0].text)
        self.assertContains(response,self.comments[1].text)

You can use a for to iterate the comments, but, for the readability of the exercise, in the book we prefer to use it in the way presented. With this, we are testing both the status code, which is the first thing we should always check, and also some of the content using the assertion type methods.

As we mentioned before, how you want to implement the tests and what to test depends entirely on the developer and you can adapt the previous exercise to your needs. Also, for the simplicity of the exercise, we do not test the pagination itself, or the records that are in the pagination and that is why we create only two records, since, if you create a third:

comments/tests.py

class ContactTests(TestCase):
    def setUp(self):
        Comment.objects.create(text='text 1')
        Comment.objects.create(text='text 2')
        Comment.objects.create(text='text 3')
        self.comments = Comment.objects.all()
        print(self.comments)
        self.client = Client()

    def test_index(self):
        response = self.client.get("/tasks/")
        self.assertEqual(response.status_code, 200)
        self.assertContains(response,self.comments[0].text)
        self.assertContains(response,self.comments[1].text)
        self.assertContains(response,self.comments[2].text)

You will see that when you run the test, it returns an error for not finding the third record since this example has two-level pagination.

Finally run the test and evaluate the result:

$ python manage.py test
Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz en Udemy

Acepto recibir anuncios de interes sobre este Blog.