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

  • проектирование моделей Django
  • выполнение операций CRUD на модели Django
  • запуск миграции Django с makemigrations и migrate
  • создание модульных тестов с помощью Python-фреймворка unittest

Цель этой статьи — поощрить автоматическое модульное тестирование моделей Django перед фиксацией схемы моделей в базе данных.

В конце этой статьи вы должны быть в состоянии:

  • создайте тестовый скрипт в экосистеме Django, чтобы протестировать базовую неразборчивость (cчтение, rчтение, updating и d >удаление) операций над моделями Django
  • выполнять тесты на основе различной степени детализации
  • выполнить тестовый скрипт с различными уровнями детализации

Модели и база данных Django

Модели Django — это представления таблиц базы данных, которые принадлежат схеме базы данных для приложения. Схема базы данных обычно состоит из таблиц, полей таблиц и их ограничений, индексов и отношений таблиц. Поскольку модель Django является абстракцией таблицы реляционной базы данных, она реализована как класс Python с данными и поведением.

Джанго Миграция

Процесс миграции Django состоит из двух этапов:

  1. makemigrations, которая создает файлы миграции для описания шагов по преобразованию класса модели Python в таблицу базы данных. Эти файлы миграции находятся в подкаталоге migrations внутри каталога приложения.
  2. migrate для реализации шагов в файлах миграции и создания соответствующих таблиц и их отношений и ограничений, если таковые имеются, в базе данных.

Перед выполнением шага 2 процесса миграции, который фиксирует наши модели в базе данных, мы должны пойти в обход и написать тесты для наших моделей Django. Тестирование модели должно охватывать:

  • валидация данных
  • модель поведения
  • модельные отношения
  • основные грубые операции

Пользователи Django могут воспользоваться преимуществами тестирования моделей с помощью различных инструментов, встроенных в Django.

Джанго БД

Поскольку наши тесты требуют, чтобы файлы миграции уже существовали для наших моделей, нам нужно выполнить шаг 1 — makemigrations процесса миграции. Мы можем проверить базу данных, чтобы убедиться, что для наших моделей не были созданы таблицы. Предполагая, что мы используем базу данных SQLite по умолчанию, мы можем вызвать команду dbshell в терминале:

$ python manage.py dbshell
SQLite version 3.36.0 2021-06-18 18:36:39
Enter ".help" for usage hints.
sqlite> .tables
sqlite>

Выполнение команды .tables внутри dbshell возвращает пустую строку, потому что мы еще не создали никаких таблиц в базе данных с помощью команды migrate.

Образец заявления

Для целей этой статьи мы собираемся определить простое приложение pet, включающее две модели, Owner и Pet, где владелец может владеть одним или несколькими домашними животными. Для простоты наши модели определяются следующим образом:

# models.py
from django.db import models
# An Owner of one or more pets
class Owner(models.Model):
    name = models.CharField(max_length=20)
    def __str__(self):
        return self.name
# A Pet must have an owner
class Pet(models.Model):
    name = models.CharField(max_length=20)
    owner = models.ForeignKey(
        to=Owner,
        on_delete=models.CASCADE  # when an owner is deleted, so is their pet
    )
    def __str__(self):
        return self.name

Обратите внимание, что параметр поля on_delete=models.CASCADE гарантирует, что объект pet будет удален при удалении объекта Owner.

Джанго тест

Сценарий тестирования по умолчанию

По умолчанию установка Django предоставляет пустой тестовый файл tests.py для каждого приложения.

$ ls pet
__init__.py   admin.py  migrations/  tests.py
__pycache__/  apps.py   models.py    views.py

Прецедент

Пустой файл tests.py может выглядеть так:

from django.test import TestCase 
# Create your tests here.

Обратите внимание, что класс TestCase импортируется из модуля test Django. Мы будем писать наши тесты объектно-ориентированным способом, создав подкласс TestCase для предоставления пользовательских данных и методов. django.test.TestCase сам по себе является подклассом unittest.TestCase Python, который позволяет тесту выполняться изолированно внутри транзакции.

Образец данных

В нашем тесте мы собираемся определить DemoTests, подкласс TestCase. Мы предоставим образцы данных внутри DemoTests, определив items, список словарей Python, сопоставляющий владельца с его питомцами как свойство класса. Например:

class DemoTests(TestCase):
    items = [
        {"owner": 'Katie', "pet": ['Toto', 'Kitty']},
        {"owner": 'Sue', "pet": ['Bunny', 'Scott']},
        {"owner": 'Lynn', "pet": ['Skylar']},
    ]

Вспомогательные свойства

Кроме того, мы собираемся определить два дополнительных свойства вспомогательного класса:

  • owner_names, список имен владельцев в items
  • pet_names, список имен питомцев в items

Например:

owner_names = [item["owner"] for item in items]
    # ['Katie', 'Sue', 'Lynn']
    pet_names = [name for item in items for name in item["pet"]]
    # ['Toto', 'Kitty', 'Bunny', 'Scott', 'Skylar']

Вспомогательные методы

Далее мы собираемся определить метод вспомогательного класса pets_by_name, который возвращает список имен домашних животных на основе имени владельца с использованием данных items. Например:

 # Given an owner name, return their a list of pets or an empty list
 def pets_by_owner(self, owner):
     for item in self.items:
        if item["owner"] == owner:
            return item["pet"]
     return []

Метод setUp()

Поскольку наши данные items представляют собой структуру данных Python, нам нужно преобразовать содержимое items в модели Django. TestCase предоставляет метод класса setUp(), который вызывается один раз для каждой транзакции. Мы можем определить метод setUp() для создания наших моделей Django на основе items. Например:

    # Create data in the database once
    def setUp(self):
        for item in self.items:
            owner = Owner.objects.create(name=item["owner"])
            for pet in item["pet"]:
                Pet.objects.create(name=pet, owner=owner)
        self.assertEqual(
            Owner.objects.all().count(),
            len(self.owner_names))
        self.assertEqual(
            Pet.objects.all().count(),
            len(self.pet_names))

Для каждого элемента в items мы используем Django QuerySet API, чтобы создать модель для Owner и Pet через objects.create(). Обратите внимание на последние два метода assertEqual(). Во-первых, нужно убедиться, что количество созданных объектов Owner эквивалентно количеству имен владельцев в items. Во-вторых, необходимо убедиться, что количество созданных объектов Pet эквивалентно количеству имен питомцев в items. Метод setUp() может быть проверкой правильности создания наших моделей.

Определить тесты

Мы успешно создали объекты модели в методе класса setUp(). Далее мы можем определить дополнительные тесты для созданных моделей как методы отдельных классов. К ним могут относиться:

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

Каждый тест должен быть небольшим и конкретным, нацеленным на конкретную единицу действия. Правило именования методов тестового класса в unittest состоит в том, чтобы добавлять к каждому тесту префикс test. Например, тест для запроса несуществующего владельца может называться test_owner_not_found().

    # Query for a non-existing owner
    def test_owner_not_found(self):
        print(f'\nRunning {self.id()}\n')
        names = list(self.owner_names)
        names.append('Lisa')
        for name in names:
            try:
                owner = get_object_or_404(Owner, name=name)
                self.assertEqual(owner.name, name)
            except Http404:
                print(f"Owner {name} not found")

Обратите внимание, что unittest предоставляет метод класса, чтобы определить имя метода тестового класса. Мы можем использовать этот метод для отображения имени теста.

unittest запускает каждый тест на основе алфавитного порядка имен тестов.

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

Если вы хотите, чтобы ваши тесты выполнялись в определенном порядке, вы должны творчески подойти к именованию тестов. Например, test_01xxxx() будет выполняться до test_02xxxx().

Чтобы увидеть образцы тестов для этой статьи, взгляните на мой репозиторий на GitHub.

Запустить тесты

Платформа тестирования Django unittest обеспечивает гибкое выполнение тестов.

Запустить все тесты

Чтобы выполнить все тесты, определенные в tests.py, в нашем приложении pet, просто введите в консоли:

$ ./manage.py test

or

$ ./manage.py test pet.tests

Запустить тестовый скрипт

Если у нас есть несколько тестовых файлов test1.py и test2.py внутри нашего приложения pet, мы можем выбрать запуск только тестов, относящихся к конкретному тестовому файлу. Например:

$ ./manage.py test pet.test2

Будет выполнять тесты только внутри test2.py.

Запустите тестовый пример

Если в нашем файле tests.py есть несколько TestCase, мы можем выбрать выполнение конкретного TestCase с именем DemoTests. Например:

$ ./manage.py test pet.tests.DemoTests

Запустите тестовый метод

Для более точной детализации мы также можем выбрать запуск только определенного метода тестирования, например test_owner_not_found(), внутри нашего тестового скрипта tests.py. Например:

$ ./manage.py test pet.tests.DemoTests.test_owner_not_found

Тестовый вывод

Детализация по умолчанию

Пример вывода с детализацией по умолчанию успешного теста может быть следующим:

Creating test database for alias 'default'... System check identified no issues (0 silenced). . . . ---------------------------------------------------------------------- Ran 3 tests in 0.022s OK Destroying test database for alias 'default'...

Тестовый уровень детализации

Если вам нужен более подробный вывод при выполнении тестов, вы можете указать аргумент -v, за которым следует число, 0, 1 (по умолчанию), 2 или 3 для команды test. Чем выше многословие, тем выше число. Например:

$ ./manage.py test -v 2

будет генерировать дополнительный вывод, например:

Creating test database for alias 'default' ('file:memorydb_default?mode=memory&c
ache=shared')...
Operations to perform:
  Synchronize unmigrated apps: messages, staticfiles
  Apply all migrations: admin, auth, contenttypes, pet, sessions
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying pet.0001_initial... OK
  Applying sessions.0001_initial... OK
System check identified no issues (0 silenced).
test_add_pet (pet.test1.DemoTests) ...
Running pet.test1.DemoTests.test_add_pet
ok
....
----------------------------------------------------------------------
Ran 7 tests in 0.022s
OK
Destroying test database for alias 'default' ('file:memorydb_default?mode=memory
&cache=shared')...

Обратите внимание, что Django migrations выполняется в тестовой базе данных default на основе файлов миграции, включая pet.0001_initial.py, созданных makemigrations . По умолчанию тестирование Django гарантирует, что тестовая база данных будет уничтожена после завершения тестирования. Для еще более подробного вывода вы можете попробовать опцию 3.

Заключение

Добавление модульного тестирования на различных этапах разработки Django, как правило, является хорошей практикой для разработчика. По мере роста веб-приложения его сложность возрастает. Помимо моделей Django, среда тестирования Django также предоставляет инструменты для тестирования представлений. Обеспечение того, чтобы каждая часть приложения Django была хорошо протестирована перед развертыванием, имеет решающее значение и является неотъемлемой частью успешного проекта веб-разработки.

Первоначально опубликовано на https://github.com.

Дополнительные материалы на plainenglish.io