How to Measure Django Code Quality Using SonarQube, Pytest, and Coverage

Greetings, fellow coding enthusiasts!

We're going to dive deep into the realm of Django code quality assessment. In this comprehensive guide, I'll walk you through an in-depth approach to measuring the code quality of your Django-based application.

By the end of this tutorial, you will be able to:

  1. Build CRUD APIs using Django and DRF (Django REST Framework)
  2. Write automated tests for the APIs using Pytest
  3. Measure code test coverage using Coverage
  4. Utilize SonarQube to assess code quality, identify code smells, security vulnerabilities, and more

Prerequisites to follow along in this tutorial include:

  1. Python 3 installation on your chosen Operating System (OS). We'll use Python 3.10 in this tutorial.
  2. Basic knowledge of Python and Django
  3. Any code editor of your choice

Without any further delay, let's jump right in and get started.

How to Get the APIs Up and Running

To begin, open your Terminal or bash. Create a directory or folder for your project using the command:

mkdir django-quality && cd django-quality

In my case, the folder name is "django-quality".

To isolate the project dependencies, we need to utilize a Python virtual environment.

To create a virtual environment, use the following command in your Terminal or bash:

python3 -m venv venv

Activate the virtualenv by running this command:

source venv/bin/activate

If everything works fine, you should see the virtual environment indicator enclosed in brackets, similar to the image shown below:

venv-activated
Python Virtualenv activated successfully

At the root directory of your project, create a folder called "requirements" that will house the external packages required for various development stages, such as dev (development) and staging.

Inside the "requirements" folder, create two files: "base.txt" and "dev.txt". The "base.txt" file will include generic packages required by the application, while the "dev.txt" file will contain dependencies specific to development mode.

By now, the contents in your project folder should have the following structure

- requirements
    ├── base.txt
    └── dev.txt
- venv

Here are the updated contents for the "base.txt" and "dev.txt" files:

base.txt

Django==4.0.6
djangorestframework==3.13.1
drf-spectacular==0.22.1

dev.txt

-r base.txt
pytest-django==4.5.2
pytest-factoryboy==2.5.0
pytest-cov==4.1.0
  • djangorestframework: Used for API development.
  • drf-spectacular : Used for automated documentation of the APIs.
  • pytest-cov: Utilized for measuring code coverage during testing.
  • pytest-factoryboy: Used for creating test data using factory patterns.

Make sure your virtual environment is activated, then run the following command at the root directory to install the dependencies specified in "dev.txt":

pip install -r requirements/dev.txt

To create a new Django project, you can run the following command:

django-admin startproject core .

The name of the project is 'core'. You can decide to use any suitable name that fits your use case.

By now, you should see a couple of files and folders automatically created after running the command.

Here is the current project structure:

├── core
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── requirements
│   ├── base.txt
│   └── dev.txt
└── venv
project-folder-structurefold-min
Current Folder Structure in VSCode

The APIs we will create will be a basic blog API with CRUD functionality. Let's create a new app within the project to host all the files related to the blog features.

Run this command to create a new app called 'blog':

python manage.py startapp blog

By now, a new folder named 'blog' has been auto-created by the command.

Here is the folder structure:

├── blog
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── core
├── manage.py
├── requirements
└── venv

Update the models.py file in the blog folder. The Blog class defines the database schema for the blog.

blog/models.py

from django.db import models

class Blog(models.Model):
    title = models.CharField(max_length=50)
    body = models.TextField()
    published = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

Create a new file named 'serializers.py' inside the 'blog' folder and update its content as shown below:

blog/serializers.py

from rest_framework import serializers

from .models import Blog

class BlogSerializer(serializers.ModelSerializer):
    class Meta:
        model = Blog
        fields = '__all__'
    
    extra_kwargs = {
            "created_at": {"read_only": True},
        }

The BlogSerializer class is utilized for validating incoming blog data sent by the client (such as from the frontend or mobile app) to ensure it adheres to the expected format.

Additionally, the serializer class is used for both serialization (converting Python objects to a transmittable format like JSON) and deserialization (converting a transmittable format like JSON back to Python objects).

Let's create the view to handle CRUD functionality, leveraging the DRF ModelViewSet to effortlessly create APIs with just a few lines of code.

blog/views.py

from rest_framework import filters, viewsets

from .models import Blog
from .serializers import BlogSerializer


class BlogViewSet(viewsets.ModelViewSet):
    queryset = Blog.objects.all()
    http_method_names = ["get", "post", "delete", "patch","put"]
    serializer_class = BlogSerializer
    filter_backends = [
        filters.SearchFilter,
        filters.OrderingFilter,
    ]
    filterset_fields = ["published"]
    search_fields = ["title", "body"]
    ordering_fields = [
        "created_at",
    ]

Create a new file named 'blog.urls' in the 'blog' folder.

By utilizing the DRF router for URL configuration, the URLs are automatically generated based on the allowed methods defined in the BlogViewSet.

blog/urls.py

from django.urls import include, path

from rest_framework.routers import DefaultRouter

from .views import BlogViewSet

app_name = "blog"

router = DefaultRouter()
router.register("", BlogViewSet)

urlpatterns = [
    path("", include(router.urls)),
]

The next step is to register the urls.py file defined in the 'blog' app within the main project's urls.py file. To do this, you should locate the project's urls.py file, which serves as the starting point for URL routing.

core/urls.py


from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import (
    SpectacularAPIView,
    SpectacularRedocView,
    SpectacularSwaggerView,
)

urlpatterns = [
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/v1/doc/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    path('api/v1/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
    path('admin/', admin.site.urls),
    path('api/v1/blogs/', include('blog.urls')),
]

The api/v1/blogs/ URL is mapped to the URLs defined in blog.urls. Additionally, other URLs are utilized for automated API documentation.

Update the settings.py file located inside the core folder. This file contains configurations for the Django application.

In the INSTALLED_APPS section, register the newly created 'blog' app, along with any desired third-party apps. Note that for brevity, the default Django apps are not included in the following list:

settings.py

INSTALLED_APPS = [


    #Third-party Apps
    'drf_spectacular',

    #Local Apps
    'blog',
]

Update the settings.py file to include configurations related to Django REST Framework (DRF) and documentation.

settings.py


REST_FRAMEWORK = {
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
    "TEST_REQUEST_DEFAULT_FORMAT": "json",
}


SPECTACULAR_SETTINGS = {
    'SCHEMA_PATH_PREFIX': r'/api/v1',
    'DEFAULT_GENERATOR_CLASS': 'drf_spectacular.generators.SchemaGenerator',
    'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'],
    'COMPONENT_SPLIT_PATCH': True,
    'COMPONENT_SPLIT_REQUEST': True,
    "SWAGGER_UI_SETTINGS": {
        "deepLinking": True,
        "persistAuthorization": True,
        "displayOperationId": True,
        "displayRequestDuration": True
    },
    'UPLOADED_FILES_USE_URL': True,
    'TITLE': 'Django-Pytest-Sonarqube - Blog API',
    'DESCRIPTION': 'A simple API setup with Django, Pytest & Sonarqube',
    'VERSION': '1.0.0',
    'LICENCE': {'name': 'BSD License'},
    'CONTACT': {'name': 'Ridwan Ray', 'email': 'ridwanray.com'},
    #OAUTH2 SPEC
    'OAUTH2_FLOWS': [],
    'OAUTH2_AUTHORIZATION_URL': None,
    'OAUTH2_TOKEN_URL': None,
    'OAUTH2_REFRESH_URL': None,
    'OAUTH2_SCOPES': None,
}

With all the necessary configurations in place, let's run the migrations command to ensure that the models in the application are synchronized with the database schema.

Execute the following commands in the root directory to synchronize the models with the database schema:

Back to Top