Django unit testing - Part (2/2): Implent unit testing and coverage in django
Table of contents
Intro
This article is using the following versions:
Django==4.2.1
djangorestframework==3.14.0
Disclaimer: This is a very on-top look over these topics, you can find it deeper in the docs.
First article link:
https://zomor.hashnode.dev/django-unit-testing-part-12-testing-and-coverage-concepts
Django architecture
Django is following the architecture pattern of (MVT) -> Model, View, Template as a full stack, where the model is containing the database models, view is containing the business logic and the mapping of the urls. And the template is the page to be rendered to the end user as the response
Main components to be tested
Mainly we'll not focus on the templates from the inside, we'll just focus on the model, view, mapping and other functions to be used
Model
To test the Django model you have to test the creation of it as the first place, also if there are any other properties you have to test them individually, another thing if you are overriding any method like the save or delete, you have to put this into consideration.
View
Mocking the view to check the response is returning as expected is one of the most important things in this testing process
Others
There are other things to be tested like the wrappers if used, the serializers, the mapping between the URL and the view, also the mapping between the view and the template.
Unit testing tools
In Django and using other libraries, we can make create lots of test cases that can cover every single detail in the project with less hustle as we will see now.
Preparing your testing environment
Inside your application directory, you can either put all your tests inside one file called (tests.py) or inside a package called (tests) where each file starts with (test_*.py)
To run your tests
python manage.py test
TestCase
This is the main class that we inherit all our test cases from, this allows a lot of internal methods that we will mention later
setUp method:
If you want to create a specific thing before starting your test, this is the correct place
tearDown method:
When you want to do anything before the end of the test, you can write it down here
from django.test import TestCase
Class TestMyName(TestCase):
def setUp(self):
pass
def tearDown(self):
pass
Override settings
If you want to override specific settings in this specific test case, just for the sake of testing (For example, you want to make sure in the debug=False, it will return a specific output)
from django.test import TestCase, override_settings
from django.conf import settings
class TestMyApp(TestCase):
def test_debug(self):
print(settings.DEBUG) # Prints False
@override_settings(**{"DEBUG": True})
def test_debug_overriden(self):
print(settings.DEBUG) # Prints True
Tags
This is used if you want to specify a group of tests together so that you can run them inclusively or exclude them
from django.test import TestCase, tag
class TestMyName(TestCase):
@tag("group_a")
def test_my_name(self):
print("group_a_test")
# Run tests for the following tag only
python manage.py test --tag=group_a
# Run tests for all except the following tag
python manage.py test --exclude-tag=group_a
Client
This is the best and fastest way to test your view, as it works internally, and doesn't require the system to be up. However, this is running your views with parameters that are not dependent on request. it returns a response object as follows:
from django.test import TestCase, tag, Client
from app.models import Student
@tag("django-test")
class TestStudent(TestCase):
def setUp(self):
self.client = Client()
def test_student_with_client(self):
Student.objects.create(name="student")
response = self.client.get("/api/students/")
print(response.__class__)
# class 'rest_framework.response.Response'
def test_student_with_client_reversed_name(self):
Student.objects.create(name="student1")
response = self.client.get(reverse("student-viewset-list"))
print(response.__class__)
# class 'rest_framework.response.Response'
Request Factory
To make use of the request object, pass parameters to it (user for example) as the tests are not running on the middleware, it's better to use this request factory
from django.test import TestCase, tag, Client
from app.models import Student
@tag("django-test")
class TestStudent(TestCase):
def setUp(self):
self.client = Client()
self.request = RequestFactory()
def test_student_with_request_factory(self):
Student.objects.create(name="student2")
req = self.request.get("/api/students/")
print(req.__class__)
# class 'django.core.handlers.wsgi.WSGIRequest'
"""
Here you can do whatever you want with the request object
req.user = User.objects.last() # for example
"""
view = StudentViewSet.as_view({"get": "list"})
response = view(req)
print(response.__class__)
# class 'rest_framework.response.Response'
Assertion
This is the crucial part in your test case, whether the test is failing or passing, you have to assert a specific thing at the time, so if the assertion is true, the test case has passed, otherwise the test case has failed
There are a lot of assertions inside this TestCase you can see a list of them there:
https://docs.djangoproject.com/en/4.2/topics/testing/tools/#assertions
from django.test import TestCase, tag, Client
from app.models import Student
@tag("assertions")
class TestAssertions(TestCase):
def setUp(self):
self.test_list = [1, 2, 3]
def test_student_creation(self):
student_name = "s"
student = Student.objects.create(name=student_name)
self.assertEqual(student_name, student.name)
self.assertIsNotNone(student)
student_two, created = Student.objects.get_or_create(name=student_name)
self.assertFalse(created)
def test_number_in_list(self):
self.assertIn(1, self.test_list)
DRF Testing
There is almost a duplication from the Django Rest Framework library for each component, that uses the same behavior as the Django ones. However, it gives more support for REST like (APIClient -> Django client, APIRequestFactory -> Django Request Factory)
Factory
Following the factory concept (Building the same thing), the design pattern of factory has been invented, to allow multiple creation of an object with less code.
Factory here is implementing the same concept, it can also be used outside of the testing part as an abstract layer for the model, or even to instantiate your object with some static data
Setting up
First you have to install as following
pip install django-factory_boy==1.0.0
Then you have to create your factory based on your model
import factory
from factory.fuzzy import FuzzyText, FuzzyChoice
from factory.faker import Faker
from app.models import GenderEnum
class StudentFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'app.Student'
""" Static data """
name = "Zomor"
gender = GenderEnum.MALE.value
""" Sequence text """
# name = factory.Sequence(lambda n: "student{}".format(n))
""" Fuzzy text """
# name = FuzzyText(length=10).fuzz()
""" Faker """
# name = Faker._get_faker().name()
""" CHOICES """
""" FUZZY CHOICE """
# gender = FuzzyChoice(choices=GenderEnum.choices()).fuzz()[1]
Foreign Key
import factory
from factory.faker import Faker
class MobileFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'app.Mobile'
student = factory.SubFactory(StudentFactory)
mobile = Faker._get_faker().msisdn()
Many To Many
import factory
from factory.faker import Faker
class CourseFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'app.Course'
name = Faker._get_faker().name()
@factory.post_generation
def students(self, create, extracted, **kwargs):
if not create or not extracted:
# Simple build, or nothing to add, do nothing.
return
# Add the iterable of groups using bulk addition
self.students.add(*extracted)
Using Factories in your code
You can use factory as follows
# Returns a User instance that's not saved
StudentFactory.build()
# Returns a saved User instance.
StudentFactory.create()
StudentFactory()
Faker
Faker is used to faking lots of random data like name, email, mobile, and more and more interesting fields
There is a package called Faker. However, factory-boy is using it inside its scope as follows
from factory.faker import Faker
faker = Faker._get_faker()
faker.name()
faker.first_name()
faker.last_name()
faker.email()
faker.country()
faker.msisdn()
Coverage
Installation and Running
First, you have to install coverage through pip
pip install coverage==7.2.5
Then you can run the same command you are using for test, just replace (python) with (coverage run) as follows
#Original test command
#python manage.py test
coverage run manage.py test
Generating reports
You can generate multiple types of reports, we will talk about the HTML and XML
# After running the previous command regarding the coverage
coverage html
# You will find the results in htmlcov/index.html
# Or if you want an xml file
coverage xml
# You will find the results in coverage.xml
Configuration
In your root directory, create this file (.coveragerc) and put your configuration as follows
[run]
source=.
omit=**/manage.py,config/*
relative_files=true
You can find the whole configuration in the docs
https://coverage.readthedocs.io/en/7.2.2/config.html
Integrating with SonarQube
Using the generated XML file, you can pass it to the sonarqube configuration in the file (sonar-project.properties) as follows
python.coverage.reportPaths=**/coverage.xml
Conclusion
Unit testing may seem a bit overwhelming. However, if you gave it the attention needed, you will double your productivity by decreasing the debugging time, also by avoiding possible bugs that you may face
You can find the project where the code snippets came from on the following github