Как измерить качество кода Django с помощью SonarQube, Pytest и покрытия

Приветствую вас, товарищи энтузиасты кодинга!

Мы глубоко погрузимся в сферу оценки качества кода Django. В этом подробном руководстве я расскажу вам о том, как измерить качество кода вашего приложения на базе Django.

К концу этого учебного курса вы сможете:

  1. Создание CRUD API с использованием Django и DRF (Django REST Framework)
  2. Написание автоматизированных тестов для API с помощью Pytest
  3. Измерение покрытия тестами кода с помощью Coverage
  4. Использование SonarQube для оценки качества кода, выявления запахов кода, уязвимостей безопасности и т.д.

Необходимые условия для работы с этим учебным пособием включают:

  1. Установка Python 3 на выбранную вами операционную систему (ОС). В данном руководстве мы будем использовать Python 3.10.
  2. Базовые знания Python и Django
  3. Любой редактор кода на ваш выбор

Давайте без промедления перейдем к делу и начнем.

Как запустить API в работу

Для начала откройте Терминал или bash. Создайте каталог или папку для вашего проекта с помощью команды:

mkdir django-quality && cd django-quality

В моем случае имя папки - "django-quality".

Для изоляции зависимостей проекта нам необходимо использовать виртуальную среду Python.

Для создания виртуальной среды используйте следующую команду в терминале или bash:

python3 -m venv venv

Активируйте virtualenv, выполнив следующую команду:

source venv/bin/activate

Если все работает нормально, то вы должны увидеть индикатор виртуальной среды, заключенный в скобки, аналогично изображению, приведенному ниже:

venv-activated

Python Virtualenv успешно активирован

В корневом каталоге проекта создайте папку "requirements", в которой будут храниться внешние пакеты, необходимые для различных стадий разработки, таких как dev (development) и staging.

Внутри папки "requirements" создайте два файла: "base.txt" и "dev.txt". Файл "base.txt" будет включать общие пакеты, необходимые приложению, а файл "dev.txt" будет содержать зависимости, специфичные для режима разработки.

К этому моменту содержимое папки проекта должно иметь следующую структуру

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

Ниже приведено обновленное содержимое файлов "base.txt" и "dev.txt":

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: Используется для разработки API.
  • drf-spectacular: Используется для автоматизированного документирования API.
  • pytest-cov: Используется для измерения покрытия кода во время тестирования.
  • pytest-factoryboy: Используется для создания тестовых данных с помощью паттернов фабрики.

Убедитесь, что виртуальная среда активирована, затем выполните следующую команду в корневом каталоге для установки зависимостей, указанных в файле "dev.txt":

pip install -r requirements/dev.txt

Для создания нового проекта Django можно выполнить следующую команду:

django-admin startproject core .

Имя проекта - 'core'. Вы можете использовать любое подходящее имя, соответствующее вашему сценарию использования.

К этому моменту вы должны увидеть несколько файлов и папок, автоматически созданных после выполнения команды.

Ниже приведена текущая структура проекта:

├── core
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── requirements
│   ├── base.txt
│   └── dev.txt
└── venv

project-folder-structurefold-min

Текущая структура папок в VSCode

API, который мы создадим, будет представлять собой базовый API блога с CRUD-функциональностью. Создадим в проекте новое приложение, в котором будут размещены все файлы, связанные с функциями блога.

Выполните эту команду для создания нового приложения под названием 'blog':

python manage.py startapp blog

К этому моменту командой была автоматически создана новая папка с именем 'blog'.

Ниже приведена структура папок:

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

Обновите файл models.py в папке blog. Класс 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)

Создайте новый файл с именем 'serializers.py' в папке 'blog' и обновите его содержимое, как показано ниже:

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},
        }

Класс BlogSerializer используется для проверки входящих данных блога, отправленных клиентом (например, из фронтенда или мобильного приложения), на соответствие ожидаемому формату.

Кроме того, класс serializer используется как для сериализации (преобразования объектов Python в передаваемый формат, например JSON), так и для десериализации (преобразования передаваемого формата, например JSON, обратно в объекты Python).

Давайте создадим представление для работы с CRUD-функциями, используя DRF ModelViewSet для легкого создания API с помощью всего нескольких строк кода.

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",
    ]

Создайте новый файл с именем 'blog.urls' в папке 'blog'.

При использовании маршрутизатора DRF для настройки URL-адресов, URL-адреса автоматически генерируются на основе разрешенных методов, определенных в 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)),
]

Следующий шаг - регистрация файла urls.py, определенного в приложении 'blog', в файле urls.py основного проекта. Для этого необходимо найти файл urls.py проекта, который служит отправной точкой для маршрутизации URL.

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')),
]

URL api/v1/blogs/ сопоставляется с URL, определенными в blog.urls. Кроме того, для автоматизированного документирования API используются и другие URL.

Обновите файл settings.py, расположенный в папке core. Этот файл содержит конфигурации для приложения Django.

В разделе INSTALLED_APPS зарегистрируйте только что созданное приложение "Блог", а также все необходимые сторонние приложения. Заметим, что для краткости стандартные приложения Django не включены в следующий список:

settings.py

INSTALLED_APPS = [


    #Third-party Apps
    'drf_spectacular',

    #Local Apps
    'blog',
]

Обновите файл settings.py, включив в него конфигурации, связанные с Django REST Framework (DRF) и документацией.

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,
}

Запустим команду migrations, чтобы убедиться, что модели в приложении синхронизированы со схемой базы данных.

Для синхронизации моделей со схемой базы данных выполните в корневом каталоге следующие команды:

python manage.py makemigrations
python manage.py migrate

Для запуска сервера разработки выполните следующую команду:

python manage.py runserver

dev-server

Запуск локального сервера разработки с помощью команды runserver

Сейчас приложение запущено по адресу http://127.0.0.1:8000/.
Для доступа к документации посетите http://127.0.0.1:8000/api/v1/doc/.

blog-doc-min-1--1

Автоматизированное документирование API блога с помощью drf-spectacular

Как писать автоматизированные тесты с помощью Pytest

Pytest, инструмент тестирования, который мы используем для написания автоматизированных тестов, включен в состав зависимостей, объявленных в папке требований. Теперь давайте напишем несколько тестов и исследуем его функциональность.

В папке blog при запуске приложения blog автоматически генерируется файл с именем "tests.py". Чтобы упорядочить тесты, создайте в каталоге blog новую папку "tests".

Переместите исходный файл "tests.py" во вновь созданную папку "tests". Чтобы сделать папку "tests" модулем, создайте пустой файл с именем "__init__.py".

Создайте новый файл с именем 'conftest.py' в папке 'tests'. В этом файле будут храниться все приспособления pytest (то есть компоненты многократного использования), необходимые в процессе написания тестов.

Структура тестовой папки:

├── tests
│   ├── conftest.py
│   ├── factories.py
│   ├── __init__.py
│   ├── __pycache__
│   └── tests.py

tests/conftests.py

import pytest
from rest_framework.test import APIClient

@pytest.fixture
def api_client():
    return APIClient()

api_client() - это фикстура Pytest, используемая для осуществления реальных вызовов API.

Создайте новый файл с именем 'factories.py'. Этот файл будет содержать фабрики, используемые при написании тестов. Фабрики предоставляют удобный способ создания объектов (то есть экземпляров моделей) без необходимости каждый раз указывать все атрибуты.

tests/factories.py


import factory
from faker import Faker
from blog.models import Blog

fake = Faker()

class BlogFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Blog
        
    title = fake.name()
    body = fake.text()
    published = True

tests/tests.py

import pytest
from django.urls import reverse
from .factories import BlogFactory

pytestmark = pytest.mark.django_db


class TestBlogCRUD:
    blog_list_url = reverse('blog:blog-list')

    def test_create_blog(self, api_client):
        data = {
            "title": "Good news",
            "body": "Something good starts small",
            "published": True
            }

        response = api_client.post(self.blog_list_url, data)
        assert response.status_code == 201
        returned_json = response.json()
        assert 'id' in returned_json
        assert returned_json['title'] == data['title']
        assert returned_json['body'] == data['body']
        assert returned_json['published'] == data['published']

    def test_retrieve_blogs(self, api_client):
        BlogFactory.create_batch(5)
        response = api_client.get(self.blog_list_url)
        assert response.status_code == 200
        assert len(response.json()) == 5

    def test_delete_blog(self, api_client):
        blog = BlogFactory()
        url = reverse("blog:blog-detail",
                      kwargs={"pk": blog.id})
        response = api_client.delete(url)
        assert response.status_code == 204

    def test_update_blog(self, api_client):
        blog = BlogFactory(published= True)
        data = {
            "title": "New title",
            "body": "New body",
            "published": False,
        }
        url = reverse("blog:blog-detail",
                      kwargs={"pk": blog.id})

        response = api_client.patch(url, data)
        assert response.status_code == 200
        returned_json = response.json()
        assert returned_json['title'] == data['title']
        assert returned_json['body'] == data['body']
        assert returned_json['published'] == data['published']
        

Класс TestBlogCRUD тестирует CRUD-функционал приложения. В классе определены четыре метода, каждый из которых тестирует определенную функциональность CRUD.

Создайте в корневом каталоге файл конфигурации Pytest с именем pytest.ini. Этот файл будет содержать настройки, указывающие Pytest, как располагать тестовые файлы.

pytest.ini

[pytest]
DJANGO_SETTINGS_MODULE = core.settings
python_files = tests.py test_*.py *_tests.py
addopts = -p no:warnings --no-migrations --reuse-db

Для запуска тестов выполните в корневом каталоге команду pytest, как показано ниже:

pytest

pytest-output

Результат выполнения тестовых примеров Pytest

Результаты тестирования показывают, что все четыре тестовых случая успешно пройдены.

На момент написания статьи двумя популярными инструментами, используемыми в сообществе Python для создания отчетов о тестовом покрытии в кодовой базе, являются Coverage и pytest-cov.

В нашем случае мы будем использовать pytest-cov из-за его гибкости при составлении отчетов о тестовом покрытии.

Создайте в корневом каталоге новый файл с именем 'setup.cfg'. Этот файл служит конфигурационным файлом для покрытия.

setup.cfg

[coverage:run]
source = .
branch = True
[coverage:report]
show_missing = True
skip_covered = True

Значение source в секции [coverage:run] задает расположение корневого каталога, из которого будет измеряться тестовое покрытие.

В дополнение к покрытию операторов в тестовом отчете, покрытие ветвей выявляет непокрытые ветви при использовании условных операторов (например, if, else, case).

Примечание: В файле setup.cfg можно указать папки, которые следует исключить из тестового покрытия, например, папки миграции. Мы настроим эти параметры в SonarQube.

Перезапустим тестовые примеры с помощью следующей команды:

pytest --cov --cov-report=xml

Опция --cov-report задает формат отчета о покрытии. Поддерживаются различные форматы, такие как HTML, XML, JSON и т.д. В данном случае мы указываем xml, поскольку он поддерживается SonarQube.

Screenshot-from-2023-07-13-05-20-43-min

Отчет о покрытии Pytest в формате XML

Для формата HTML в корневом каталоге будет создана папка с именем 'htmlcov'. Эта папка содержит файл 'index.html', который позволяет визуализировать результаты покрытия и области, которые не покрыты.

Как настроить SonarQube

SonarQube - инструмент, используемый для статического анализа кода. Он помогает выявлять проблемы качества кода, ошибки, уязвимости и запахи кода в программных проектах.

Для упрощения процесса мы можем запустить Docker-контейнер на основе образа SonarQube.

Выполните следующую команду в командной строке:

docker run -d -p 9000:9000 -p 9092:9092 sonarqube

Через несколько мгновений, в зависимости от скорости Вашего Интернета, зайдите на сайт http://0.0.0.0:9000/.

Для доступа к приложению можно использовать следующие учетные данные: Имя пользователя: admin Пароль: admin

Далее необходимо загрузить программу Sonar Scanner. Перейдите по этой ссылке и выберите вариант, совместимый с вашей операционной системой (ОС).

sonar-scanner

Загрузка программы SonarScanner на сайте Sonarsource.com

Распакуйте сонар-сканер и переместите его из папки 'Downloads' в защищенный каталог .

unzip sonar-scanner-cli-4.8.0.2856-linux.zip

mv sonar-scanner-4.2.0.1873-linux /opt/sonar-scanner

Добавьте следующие строки в содержимое файла sonar-scanner.properties, расположенного по адресу /opt/sonar-scanner/conf/sonar-scanner.properties:

vim  /opt/sonar-scanner/conf/sonar-scanner.properties

Добавьте эти две строки и сохраните файл:

sonar.host.url=http://localhost:9000
sonar.sourceEncoding=UTF-8

Добавьте /opt/sonar-scanner/bin в переменную окружения PATH системы, выполнив следующую команду:

export PATH="$PATH:/opt/sonar-scanner/bin

Обновить содержимое .bashrc:

vim ~/.bashrc

Добавьте эту строку в файл .bashrc и сохраните его:

export PATH="$PATH:/opt/sonar-scanner/bin

Для применения изменений к текущему сеансу терминала выполните следующую команду:

source ~/.bashrc

Для того чтобы убедиться, что все работает правильно, выполните следующую команду:

sonar-scanner -v

scanner2

Проверка версии sonarqube на терминале

Перейдите на вкладку "Проекты" на приборной панели SonarQube и перейдите к ручному созданию нового проекта.

WhatsApp-Image-2023-07-13-at-06.22.56

Создание нового проекта на приборной панели sonarqube

Задайте подходящее имя проекта, затем выберите опцию "Использовать глобальную настройку", прежде чем приступить к созданию проекта.

create-suitable-name

Выбор подходящего имени в качестве имени нового преоекта

create-globa-setting

Конфигурирование нового проекта для использования глобальных настроек

После создания проекта вам будет предложено выбрать метод анализа для вашего проекта. Выберите опцию 'Locally'.

WhatsApp-Image-2023-07-13-at-06.34.22

Запустить анализ проекта локально

После выбора опции 'Locally' потребуется сгенерировать токен. Для продолжения работы нажмите кнопку "Продолжить". Далее выберите язык программирования проекта и операционную систему (ОС), на которой он будет работать.

lang-os

Выбор языка программирования проекта и ОС

Скопируйте отображаемую команду, поскольку мы будем использовать ее для выполнения анализа проекта.

WhatsApp-Image-2023-07-13-at-06.38.29

Код, необходимый для запуска анализа

Вот содержание команды:

sonar-scanner \
  -Dsonar.projectKey=newretailer \
  -Dsonar.sources=. \
  -Dsonar.host.url=http://0.0.0.0:9000 \
  -Dsonar.token=sqp_7b6aada8ce53e97ebb7b2bf5e9b64d53b8938a6f \
  -Dsonar.python.version=3

Примечание: В команду добавлена дополнительная строка для указания версии Python в виде -Dsonar.python.version=3.

Перед выполнением команды анализа выполните следующие действия:

  1. Нажмите на кнопку "Настройки проекта" и выберите "Общие настройки".
  2. Далее перейдите на вкладку "Область анализа".

source-file-exclusion

Исходные файлы, которые должны игнорироваться анализом

Исключения исходных файлов используются для указания файлов или папок, которые SonarQube не должен анализировать как часть кодовой базы. Они могут включать файлы или каталоги, которые не являются непосредственной частью кода, но все же присутствуют в каталоге проекта.

Примерами таких файлов или папок являются:

  • venv (virtualenv)
  • htmlcov (формат HTML покрытия)
  • node_modules (каталог модулей Node.js)

Исключения кодового покрытия используются для указания файлов или папок, которые должны быть исключены при расчете процента покрытия.

Ниже приведены шаблоны игнорируемых файлов и папок:
**/tests/**, **/migrations/**, **/admin.py, **/apps.py, core/asgi.py, core/wsgi.py, manage.py

<фигура>Screenshot-from-2023-07-13-06-55-38

Паттерны, используемые для исключения некоторых файлов из отчета о покрытии и расчета процента покрытия

На вкладке "Languages" выберите "Python" в качестве языка программирования для проекта. Затем обновите путь к отчету о покрытии как "coverage.xml".

uo

Выбор языка программирования и расположение отчета о покрытии XML

Выполните ранее предоставленную команду в корневом каталоге:

sonar-scanner   -Dsonar.projectKey=DjangoSonar   -Dsonar.sources=.   -Dsonar.host.url=http://0.0.0.0:9000   -Dsonar.token=sqp_bb1dc2534249bf567c681f4acc440c2e278cb43f   -Dsonar.python.coverage.reportPaths=coverage.xml -Dsonar.python.version=3

Если все работает правильно, то вы должны увидеть успешный результат.

sonarsuccess

Запуск анализа sonarqube на проекте с помощью команды, заданной на панели управления

Если при попытке локального анализа проекта возникают ошибки, связанные с несанкционированным доступом или проблемами с правами, выполните следующие действия:

  1. Войдите в интерфейс администратора SonarQube.
  2. Перейдите в раздел 'Security'.
  3. Найдите опцию 'Force user authentication' и отключите ее.
  4. Сохраните изменения и повторно запустите анализ, используя предыдущую команду.

force-user-auth

Отладка ошибки аутентификации при анализе проекта

Другой способ устранения ошибок - посещение уведомлений о предупреждениях и проверка ошибок, возникших в ходе анализа проекта.

WhatsApp-Image-2023-07-13-at-07.50.30

Предупреждающие сообщения для анализа

Нажмите на кнопку "Общий код", чтобы перейти к разделу общего анализа кода:

Screenshot-from-2023-07-13-07-18-26-1

Результат анализа SonarQube для проекта на приборной панели

Вернуться на верх