In the development of any software, whether it’s desktop software, a mobile app, or a web app, testing plays an important role. Doing proper testing guarantees that your application won’t crash or have bugs in it. Testing can be performed in various ways like writing a script (that’s called automated testing) or manually going through each of the functionality and just looking at the user interface performing some action and doing whatever your application is built for. The main purpose of your testing should be to crash the application in every possible way. In this tutorial, we are going to look at the basics of how to write Django unit test cases.
Each type of software has its own testing processes, tools, and rules. But here, we will only discuss testing web applications written in Django or Django Rest Framework.
Table of Contents
Types of testing
Before discussing testing Django applications, let’s discuss some of the general types of testing.
Black box testing
In this type of testing, we only test the user interface and check if the application is doing what was expected. We just go through each and every phase of the app and try to use it as a consumer.
Performance testing
This includes the testing of your app’s performance. For example, if you are testing an API, we test how long it’s taking to send the response, how many databases queries it’s executing in the database, etc. We just set up strict standards and go by them to see if the module is satisfying those standards or not.
Unit tests
This type of testing includes breaking down your application into multiple functionalities and just testing them one by one. Just like we have a whole authentication system implemented. So what we will do is break it into multiple functionalities like registration, login, etc. Then we test them separately. This is what we are going to discuss in this article.
User interface testing
GUI testing plays an important role in your testing cycle because that’s what end-users are going to look at. UI testing includes checking the cross-browser and cross-screen compatibility of each and every page of your web app.
Security testing
Security testing plays an important role in the testing of your application. These tests ensure that all the authentication and permission systems are working fine. One of the common types of security testing is penetration testing. Which is an automated cyber-attack used to test if your system handles unwanted users or not.
Regression testing
This includes that the latest changes in the code haven’t affected or crashed the existing code in the application. Unit tests are also a kind of regression testing that checks whether all functionalities are behaving the same as they are expected to do.
Look to Hire Django Development Team?
Share the details of your request and we will provide you with a full-cycle team under one roof.
Writing Django unit test
Testing web applications is one of the hardest tests in the software. Because this includes testing the HTTP requests, then the security of your application, then we have to test whether the functionality is working appropriately, it should cover each and every condition. First, it should test the True condition and then the false one. And finally testing the response whether it’s giving proper HTTP status codes, using the right template, etc.
In order to write test cases in Django, you should use the unittest library. But Django has its own UnitTest class that we can use to write unit tests. In this tutorial, we will be using the “django.test.TestCase” class which inherits from the “unittest.TestCase” class. This class runs the test cases in transactions to keep them isolated.
Here is a simple example of a test case:
from django.test import TestCase from authentication.models import User class AuthenticationTestCase(TestCase): def setUp(self): User.objects.create(username='testuser') def test_user_created(self): user = User.objects.filter(username='testuser') self.assertTrue(user.exists())
Once you have written tests, you can use the “test” management command to run your test cases
$ python manage.py test
This command will run all the test cases of all the apps in the project. Additionally, you can run specific tests by passing the appropriate label to the command
# Run all the tests in the authentication.tests module $ python manage.py test authentication.tests # Run all the tests found within the authentication package $ python manage.py test authentication # Run just one test case $ python manage.py test authentication.tests.AuthenticationTestCase # Run just one test method $ python manage.py test authentication.tests.AuthenticationTestCase.test_user_created
Test case conventions
There are certain conventions for writing test cases that I personally follow to write efficient test cases that cover all the functionalities and execute each line of code.
- We must follow the correct directory structure for test cases. This is not a convention but a requirement. Your test case class should be written in the file following the *test*.py pattern. It’s a good practice to keep the test cases of each app in their respective modules. If you are writing simple test cases then you can make a file named tests.py in your app’s module and write your test cases. If there are complex test cases that are suitable in each individual file then you can make a python directory named tests in your app and add multiple test files following the *test*.py naming pattern. Following this point is necessary so that the tests library can recognize your classes as test classes.
- The class name of your unit test class should tell you about the functionalities that you are testing in that class, followed by “TestCase” postfix. For example, if you are writing test cases for Authentication, your test case class name should be “AuthenticationTestCase”. So that when any of the tests fail, you can guess which functionality is failing by only reading the name of the class in the console.
- Each method in your test case class should test each functionality separately. For example, if you are testing a view with a form, you must test the forms bypassing valid and invalid data. For both types of data, you should write two separate methods. And your method name should reflect the functionality that you are testing beginning with the “test” prefix so that when the test fails you can guess which functionality is failing by merely reading the failed method name. For example, if you are testing login, you should first pass valid data and then invalid data. For these, you can name your methods test_login_valid_data and test_login_invalid_data respectively. And when your view has a condition in it, your test case should cover both passing and failing conditions and you have to write separate methods for this. The same is the case with exceptions.
- If your view requires the user to be logged in, then your test case should also cover testing the view with a non-logged-in user and then with a logged-in user.
- If you are using permissions, then you should test your view by giving the user appropriate permissions and then by removing the permissions.
- When testing views, the convention is that you should write separate classes for each view. In this way, it is easy to maintain the test cases.
Database in test cases
Test cases create and use their own database. Each time tests are run, Django creates a test database, runs all the migrations on it, and then it starts running the test cases. After the tests are completed, whether they pass or fail, the test database is destroyed. You can prevent the destruction and continue using the same database each time bypassing the “–keepdb” flag.
Test cases lifecycle
Django’s TestCase class follows a certain lifecycle that runs when each test case class is executed. The TestCase class provides certain methods that we can use to initialize content before the test cases are run.
- First, the test database is created and all the migrations are run
- Django begins the execution of each test case class present in tests.py files
- At the beginning of each class’s execution, Django runs the setUpClass method which is a class method. You can create model instances or initialize data in this method that will be available to all the test methods of the class. This method is run only one time when the class is initialized.
- After that, the setUp method is run before running each test method. For example, if you have 3 test methods, the setup method will run three times. This method is also used to prepare the database for the test method. You can create objects to use in test methods.
- Once the test method is completed, Django runs the teardown method. This method is run after each test method is completed. setUp is run before the test method and teardown runs after each test method. You can override this method to delete the objects created in the database.
- Once all the test methods are completed, the tearDownClass method is executed.
- After all the test case classes are executed, the database is destroyed.
Mocking and patching
Sometimes in your functionality, you are using certain third-party libraries or performing certain actions that are not appropriate to test. For example, if you are sending an email or performing a Stripe transaction, you cannot do that while running test cases. For these types of issues, unittests provide mock tests. It allows you to replace certain parts of your functionality with mock objects and you can write the test case to see whether the mock object was called or not.
For example, you have this simple view:
class TestView(FormView): def form_valid(self, form): # send email here mail = EmailMessage() mail.send() return form
In your test case, you don’t want an email to be sent, so for that unit test provide a patch decorator that replaces the certain line with a Mock object.
class MockTestCase(TestCase): @patch('TestView.EmailMessage.send') def test_mock_email_message(self, mock_object): self.assertTrue(mock_object.called)
Testing Django Models
For testing Django models, you have to test basic CRUD operations using Django ORM. If you have overridden model methods like save, you can test the functionality written in that method too. You have to individually test validations for each field whether it is required or not etc. You have to do it by passing invalid and valid data both
For example:
class Test(models.Model): test = models.CharField(null=False, blank=False, ...) class ModelTestCase(TestCase): def test_creation_invalid(self): self.assertException(Test.objects.create(), IntegrityError) def test_creation_valid(self): t = Test.objects.create(test='abc') self.assertEquals(t.test, 'abc')
Testing Django Forms
Testing Django forms is the same as testing models, you have to test all the validation bypassing correct and incorrect data individually. You have to check whether the correct error is returned by the forms or not.
class TestForm(forms.ModelForm): class Meta: model = Test fields = '__all__' class FormTestCase(TestCase) def test_invalid_data(self) data = {} f = TestForm(data=data) self.assertFalse(f.is_valid()) self.assertIn('test', f.errors) self.assertEquals(f.errors['test'], 'This field is required') def test_valid_data(self): data = {'test': 'abc'} f = TestForm(data=data) self.assertTrue(f.is_valid())
Testing Django Views
As discussed earlier, testing Django views is the most crucial phase of your testing because this is where the user interacts. Your test cases should cover all the authentication and permissions functionalities and then after that they should cover your main logic of the view. For testing views, the Testcase class provides a client variable through self.client. We can use that to send an HTTP request to your views.
class TestView(generics.TemplateView): queryset = Test.objects.all() template_name = 'test/list.html' # you have the url for this view as '/tests' class ViewTestCase(TestCase): def test_view(self): response = self.client.get('/tests') self.assertEquals(response.status_code, 200) self.assertTemplateUser(response.template, 'test/list.html')
Testing Django Rest Framework
The method of writing test cases for Django Rest Framework is the same as writing test cases for Django. Except in Django, you have to check for the correct templates, etc. In testing DRF, you have to test for the right HTTP status code that the API is returning and check for the data that the API is returning if it is valid or not.
For example, let’s consider a certain API with a URL as “/input”, which accepts both GET and POST methods. When the API is called with the POST method it requires a certain key named ‘test’ to be present in the request; otherwise, it returns a 400 response. If the request is valid, it returns a 201 status code and stores the data in the database. When the API is called with GET and the user is logged in, it returns the value of “test” given by the last POST request. So its test cases would be like this:
from django.test import TestCase class ExampleTestCase(TestCase): def setUp(self): token = login_user() self.auth_headers = {"Authorization": token} def test_input_unauthorized(self): response = self.client.get("/input") # if the user is not logged in, unauthorized (401) is returned self.assertEquals(reponse.status_code, 401) def test_input_authorized(self): response = self.client.get("/input", headers=self.auth_headers) self.assertEquals(response.statuse_code, 200) # we haven't posted any data yet so response is None self.assertIsNone(response.data) def test_input_invalid_data(self): data = {} response = self.client.post("/input", headers=self.auth_headers, data=data) self.assertEquals(response.statuse_code, 400) def test_input_valid_data(self): data = {'test': 'abc'} response = self.client.post("/input", headers=self.auth_headers, data=data) self.assertEquals(response.statuse_code, 201)
This is how you write the test cases for your web app. The more strict you write your test cases the more secure your app should be.
Other libraries used for test cases
If you have any issues using the unittests library or Django’s default test case class, you can use some other helper libraries that will make your life easy to write test cases:
Pytest
Pytest makes it easy to write and maintain test cases. It provides detailed information about the failing assert expression. It automatically discovers test case modules and functions. You can read more about Tests from their documentation.
model mommy
A time comes in writing test cases when you want to create multiple instances of a model and for the required fields, you have to pass the data. Model mommy helps you create instances without the hustle of passing long and huge data. You can just pass the desired field’s values and the model mommy will handle all other fields. It makes the life of developers easy.