If you are a developer, you already know how important testing is in any software project. In particular, automatic testing, as it can help you to corroborate your coding, and quickly see that your program does what you want it to do.
It also helps to corroborate any new changes in your code that didn't break any previous functionalities.
In saying that, does this mean that if you have automatic tests then your project won't have any errors? It does not. As computer scientist Edsger W. Dijkstra once said:
“Program testing can be used to show the presence of bugs, but never to show their absence”.
This phrase helps us to understand that we can't be sure that a program is perfect, but, testing is fundamental to help us discover errors and make fixes and improvements.
As a person that works in software development, I can say that it's a lot better when you discover an error rather than your client or a user in production.
In this article, I'll talk about basic knowledge in automatic testing, useful tools, and good practices in Django projects, with a focus on API's.
Testing in Django & Django REST Framework
Django has very nice documentation about testing, and Django REST Framework too. So, in this blog, I'll talk about the main tools in those two frameworks, and what you can do and use to improve your testing.
Main Testing Classes
Django provides several classes for testing. The one I'll talk about here is TestCase, which is very useful if your application uses databases.
As the name states, to create a test case in your Django project, you will define a class that inherits from TestCase.
By doing this, you can use all the methods and properties of the named class that will help you to create and execute your tests. Then you will define different functions that will correspond to each unit test inside the test case.
Let's see an example of the structure of a test case:
Also, you can use the Client class of Django which simulates a dummy browser so you can make HTTP requests and test how your Django API responds.
This client extends the TestClient, meaning that it has the same functionalities and adds others such as the [.c-inline-code]credentials[.c-inline-code] function. This function is very useful to overwrite authentication headers, for example, using OAuth1, OAuth2, or any simple token authentication scheme.
So, if you are working on a project with the Django REST framework, you can change the previous example in this way:
Main Testing Functions
The aforementioned classes each have a set of methods that will help you through the testing. With these functions, you are able to corroborate that your software works as expected and make necessary test fixtures.
A test fixture is an environment that you can create to run your tests using consistency. Using fixtures you can make sure that certain conditions are met before the execution. For example, to have a determined set of data in your testing database, or to create needed objects. Here is a list of the main ones:
- [.c-inline-code]setUp[.c-inline-code] and [.c-inline-code]tearDown[.c-inline-code]: These are functions to be executed before (setUp) and after (tearDown) each unit test. These are very useful for fixtures.
- [.c-inline-code]setUpClass[.c-inline-code] and [.c-inline-code]tearDownClass[.c-inline-code]: In an analogous way of setUp and tearDown, these functions are executed before (setUpClass) and after (tearDownClass) during the whole test case. This means that it is executed only once for a test case. Since they're used by Django to make important configurations, if you overwrite these methods in your test case class, don't forget to call the [.c-inline-code]super[.c-inline-code] implementation inside the functions.
- [.c-inline-code]setUpTestData[.c-inline-code]: this function can be used to have a class-level atomic block to define data for the whole test case. That means that this function automatically rollbacks the changes in the database after the finalization of all the unit tests in the test case. This is mainly used to load data to your test database.
- Assert methods: Django has the set of assert methods from unittest, and has other ones created for the framework. These methods, such as [.c-inline-code]assertEqual[.c-inline-code], [.c-inline-code]assertIsNone[.c-inline-code], [.c-inline-code]assertTrue[.c-inline-code], etc, can be used in the unit test to check the different conditions that have to be met. For example, in the unit test of login, you can assert that the response has a status code of 200 because you want to make sure that if the data is sent correctly, then your application has to return the response with that code.
You can avoid using these functions, or use any combination of them. They're not needed, but they're very useful and will help you a lot.
Test Discovery & Databases
Where do you have to put your test classes? By default, Django recognizes any file that fulfills the pattern test*.py under the current working directory. That is, any file under the directory that starts with the [.c-inline-code]test[.c-inline-code] substring, and of course, has the [.c-inline-code].py[.c-inline-code] extension. Inside it, Django will execute any function in the test case class starting with [.c-inline-code]test[.c-inline-code].
When you run your tests, you can pass a parameter indicating the desired pattern just in case you want to use a different one. With that behavior, the place to put the test cases can be anywhere, but I recommend dividing the tests into the several Django apps that you may have in your project.
For example, if you have a Django app called [.c-inline-code]users[.c-inline-code] that has the models and functionalities related to the users in your project, you can define in there a [.c-inline-code]test[.c-inline-code] folder as a module (adding the [.c-inline-code]__init__.py[.c-inline-code] file) with all the tests that cover that important part: tests for login, for sign-up, for user data, account deletion, etc.
It would look like something similar to this:
Regarding the databases, when you run your tests by default, Django creates one and then destroys it after the finalization. This is to avoid conflicts between your production and/or development databases, as well as your testing database.
You can define and customize a database for tests, inside the [.c-inline-code]DATABASES[.c-inline-code] dictionary, adding another one named [.c-inline-code]TEST[.c-inline-code].
Something like this:
Take a look at the available keys for the [.c-inline-code]TEST[.c-inline-code] dictionary. If you don't define it, Django will create a database naming it as equal to your database in the [.c-inline-code]default[.c-inline-code] settings, adding the [.c-inline-code]test_[.c-inline-code] prefix.
Faker is a python package used to generate fake but realistic data. You can use its functionalities to load data into your testing database, generate data for the requests that you want to test, generate data for the models, etc. Faker has a huge and diverse set of possibilities. You can generate during the execution time, for example, first names, last names, phone numbers, dates, passwords, emails, etc.
Additionally, you can pass parameters to the functions to generate data with different constraints, for example, you can obtain a password specifying if you want to use special characters or not, the desired length, and if you want to have an upper case or not, etc.
Take a look at the Faker providers to see all the different fake but realistic data that you can generate for your tests. Instead of using for example [.c-inline-code]firstname.lastname@example.org[.c-inline-code] for your test cases, you can have data that is closer to reality and improve the quality.
If you use factories for tests, you define in a class the data structure that you want to have in your testing database. Then in your tests, it will be easier to have instances of your models, and loaded data with the desired structure.
Factory Boy is a tool to replace static, hard-to-maintain fixtures using factories. Also, you can combine it with Faker to have factories with fake but realistic data. Let's see an example:
In this case, we can see a static fixture of Django, to load data to the testing database in a model called Person, which has a first name and last name.
This JSON file defines two instances:
Now let's see the analogous definition using Factory Boy and Faker:
With this, you can use functions that Factory Boy provides, for example, [.c-inline-code]PersonFactory.create_batch(2)[.c-inline-code] to create instantly 2 instances in your testing database of Persons with different and realistic first and last names. In the JSON case, if you want to have for example 10 instances, you will need to add them and define new first and last names explicitly.
And, if you want to have huge datasets, the JSON file will be enormous and hard to maintain, while in the factory case you only need to call a function. Also, what if the model Person changes? In the JSON case, you have to update one by one each defined instance.
In the factory case, you only need to change a class definition. Factory Boy documentation has a section called common recipes, where you can find useful tools, practices, and tips from the library. Here is a list of interesting functionalities:
- You can create factories of models including the model relationships using the [.c-inline-code]RelatedFactory[.c-inline-code] or [.c-inline-code]SubFactory[.c-inline-code] class.
- Use the [.c-inline-code]create[.c-inline-code] function to create a new instance of the model in the factory and save it into the testing database. If you want to have instances but without saving them into the database, use [.c-inline-code]build[.c-inline-code] instead.
- In an analogous way, you can create and save into the database N instances of the model in the factory using [.c-inline-code]create_batch(N)[.c-inline-code], and [.c-inline-code]build_batch(N)[.c-inline-code] if you don't want to save them.
- You can define an attribute in a factory that selects a choice of a set of choices just like a Django model choice field, using [.c-inline-code]random_element[.c-inline-code] of Faker. In the next section, we will see an example of this.
Test Example & Good Practices
In this section, I want to wrap up and show a little example. This is the tiny reality: In the project, we have a Django app called users where we have the user model that has a username, phone_number, and a category in the system, that could be admin, common user, and guest. The user can sign-up and login into the application.
Model & Factory Definition
Now, let's see the implementation of the user factory. This file is inside a test folder in the users Django app:
A couple of things to note about this:
- In the [.c-inline-code]Meta[.c-inline-code] class inside the factory, we have to indicate the corresponding model.
- In the category attribute, we use [.c-inline-code]random_element[.c-inline-code] of Faker to randomly select a choice of the Category choice list. We've defined [.c-inline-code]CATEGORIES_VALUES[.c-inline-code] to specify which part of the choice we want to take. That is because each element in the choices list is a tuple, where the first part is the value, and the second one the explanatory text. So, that [.c-inline-code]CATEGORIES_VALUES[.c-inline-code] set has only that first part for each element.
- Through the [.c-inline-code]post_generator[.c-inline-code] decorator and the [.c-inline-code]password[.c-inline-code] function, we indicate that at the moment of the creation of a user instance, we can pass a desired password to be used, or else we generate one using the Faker functionality.
Django Test Case
Now, let's see an example of testing for the user sign-up functionality. The implementation of the authentication part for this example project has been made using dj-rest-auth. I won't talk about that, but if you want to know more, please take a look at this blog.
This is a [.c-inline-code]test_singup.py[.c-inline-code] file inside the test folder of the user Django app:
Well, in this file we have a test case, defined as UserSignUpTestCase, and two unit tests inside, one to test the expected case, and the other to test an error case. I want to list a couple of main points to discuss good practices:
- Take a look at the unit tests names: You can name them as you want (always starting with [.c-inline-code]test[.c-inline-code] unless you change the pattern). A good practice is to make a name that describes it. This will help you when testing and debugging, for example, if a test fails and you want to trace the error.
- In the setUpClass method, we call the [.c-inline-code]super()[.c-inline-code] function, remembering that it's important for Django. Also, we set up the needed instances, such as the API client, a user, and a faker instance. Besides we load a user to the database with the [.c-inline-code]create[.c-inline-code] function.
- Notice that the [.c-inline-code]sign_up[.c-inline-code] URL defined in the setUpClass method, uses [.c-inline-code]reverse[.c-inline-code] from Django. You can put directly the URL string if you want. But using reverse, you obtain an URL entering the related name. In the dj-rest-auth package, the related name is [.c-inline-code]rest_register[.c-inline-code]. I recommend you to use reverse in your project to improve maintainability. When you define URLs, give them a name, and then in the tests use reverse. After that, if a URL changes, you won't need to change anything in the tests, because reverse solves this.
- The general structure chosen for the unit test is:
- Prepare data: You can use the instance generated with [.c-inline-code]build[.c-inline-code] to obtain the data for the request.
- Make the request: Use the APIClient to make the request.
- Check the response: Check the response data (if there is data) and status. When working with Django REST framework, I recommend using status. With this, you have the HTTP status codes in a cleaner way. If you don't want to, you can just put the number directly.
- Check the database: If the request generates changes in the database, please corroborate it to make sure that the functionality modifies it correctly. If you have the case that doesn't modify the database, it's a good practice to test that the database remains without changes (just as in the second unit test).
This was an example of a Test Case with two unit tests. You can add more unit tests, for example, to test if some data is incorrect in the request so the user can't sign-up. Besides you can add in that file (or another file) more test case classes, for example, to test the login functionality of the app.
Running Your Tests
Once you have coded a couple of tests, you can execute them with this command in the main folder of the project (the one that has the manage.py file):
- [.c-inline-code]python manage.py test[.c-inline-code] This will search for all of your tests files under that folder and then execute them, showing the error or success of each unit test.
Customize the Execution of the Tests
You can customize the test execution, for example, you can indicate to test:
- Only a Django app, specifying [.c-inline-code]<Django app name>[.c-inline-code]:
[.c-inline-code]python manage.py test users[.c-inline-code]
- Only a file in a given Django app, specifying [.c-inline-code]<Django app name>.path.to.file[.c-inline-code]:
[.c-inline-code]python manage.py test users.test.test_signup[.c-inline-code]
- Only a test case class, specifying: [.c-inline-code]<Django app name>.path.to.file.ClassName[.c-inline-code]:
[.c-inline-code]python manage.py test users.test.test_signup.UserSignUpTestCase[.c-inline-code]
- Only a unit test function, specifying [.c-inline-code]<Django app name>.path.to.file.ClassName.function_name[.c-inline-code]:
[.c-inline-code]python manage.py test users.test.test_signup.UserSignUpTestCase.test_if_data_is_correct_then_signup[.c-inline-code]
You can also add some flags to the command to customize the execution, for example:
- [.c-inline-code]--keepdb[.c-inline-code] to avoid erasing the database between executions. This can improve the speed.
- [.c-inline-code]--parallel[.c-inline-code] to run the tests in parallel improving also the speed on multi-core hardware. If you do this make sure they're well isolated.
Throughout this piece, I've presented some basics of testing with Django and Django REST framework. Also, I showcased some useful tools and good practices that are more focused on API's. I highlighted some examples, and how to run and customize your tests with the named framework.
I hope you enjoyed reading and I hope that this blog can help you to test and improve your own Django API projects. Feel free to let us know the comment section what you think of this approach.