Django unit testing - Part (2/2): Implent unit testing and coverage in django

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

https://github.com/elZomor/unit_testing_project