Русский
Русский
English
Статистика
Реклама

Перевод Новое тестирование фичей в Django 3.2


Пару недель назад Django 3.2 выпустил свой первый альфа-релиз, а финальный релиз выйдет в апреле. Он содержит микс новых возможностей, о которых вы можете прочитать в примечаниях к релизу. Эта статья посвящена изменениям в тестировании, некоторые из которых можно получить на более ранних версиях Django с пакетами backport.

1. Изоляция setUpTestData()

В примечании к релизу говорится:

Объекты, назначенные для классификации атрибутов в TestCase.setUpTestData() теперь выделяются для каждого тестового метода.

setUpTestData() очень полезный прием для быстрого выполнения тестов, и это изменение делает его использование намного проще.

Прием TestCase.setUp() нередко используется из юнит-теста для создания экземпляров моделей, которые используются в каждом тесте:

from django.test import TestCasefrom example.core.models import Bookclass ExampleTests(TestCase):    def setUp(self):        self.book = Book.objects.create(title="Meditations")

Тест-раннер вызывает setUp() перед каждым тестом. Это дает простую изоляцию тестов, так как берутся свежие данные для каждого теста. Недостатком такого подхода является то, что setUp() запускается много раз, что при большом объеме данных или тестов может происходить довольно медленно.

setUpTestData() позволяет создавать данные на уровне класса один раз на TestCase. Его использование очень похоже на setUp(), только это метод класса:

from django.test import TestCasefrom example.core.models import Bookclass ExampleTests(TestCase):    @classmethod    def setUpTestData(cls):        cls.book = Book.objects.create(title="Meditations")

В промежутках между тестами Django продолжает откатывать (rollback) любые изменения в базе данных, поэтому они остаются там изолированными. К сожалению, до этого изменения в Django 3.2 rollback не происходили в памяти, поэтому любые изменения в экземплярах моделей сохранялись. Это означает, что тесты не были полностью изолированы.

Возьмем, к примеру, эти тесты:

from django.test import TestCasefrom example.core.models import Bookclass SetUpTestDataTests(TestCase):    @classmethod    def setUpTestData(cls):        cls.book = Book.objects.create(title="Meditations")    def test_that_changes_title(self):        self.book.title = "Antifragile"    def test_that_reads_title_from_db(self):        db_title = Book.objects.get().title        assert db_title == "Meditations"    def test_that_reads_in_memory_title(self):        assert self.book.title == "Meditations"

Если мы запустим их на Django 3.1, то финальный тест провалится:

$ ./manage.py test example.core.tests.test_setuptestdataCreating test database for alias 'default'...System check identified no issues (0 silenced)..F.======================================================================FAIL: test_that_reads_in_memory_title (example.core.tests.test_setuptestdata.SetUpTestDataTests)----------------------------------------------------------------------Traceback (most recent call last):  File "/.../example/core/tests/test_setuptestdata.py", line 19, in test_that_reads_in_memory_title    assert self.book.title == "Meditations"AssertionError----------------------------------------------------------------------Ran 3 tests in 0.002sFAILED (failures=1)Destroying test database for alias 'default'...

Это связано с тем, что in-memory изменение из test_that_changes_title() сохраняется между тестами. Это происходит в Django 3.2 за счет копирования объектов доступа в каждом тесте, поэтому в каждом тесте используется отдельная изолированная копия экземпляра модели in-memory. Теперь тесты проходят:

$ ./manage.py test example.core.tests.test_setuptestdataCreating test database for alias 'default'...System check identified no issues (0 silenced)....----------------------------------------------------------------------Ran 3 tests in 0.002sOKDestroying test database for alias 'default'...

Спасибо Simon Charette за изначальное создание этой функциональности в проекте django-testdata, и до его объединения в систему Django. На старых версиях Django вы можете использовать django тест-данные для той же изоляции, добавив декоратор@wrap_testdata в ваши методы setUpTestData(). Он очень удобен, и я добавлял его в каждый проект, над которым работал.

(В более старых версиях Django также документируется обходной путь, включающий повторный запрос базы данных в каждом тесте, но это медленнее).

2. Использование faulthandler по умолчанию

В примечании к релизу говорится:

DiscoverRunner сейчас использует faulthandler по умолчанию.

Это небольшое улучшение, которое может помочь вам дебаггить сбои низкого уровня. Модуль Python faulthandler предоставляет способ сброса экстренной трассировки в ответ на проблемы, которые приводят к сбою в работе интерпретатора Python.Тогда тестовый runner Django использует faulthandler. Подобное решение было скопировано из pytest, который делает то же самое.

Проблемы, которые ловит faulthandler, обычно возникают в библиотеках на языке C, например, в драйверах баз данных. Например, OS сигнал SIGSEGV указывает на ошибку сегментации, что означает попытку чтения памяти, не принадлежащей текущему процессу. Мы можем эмулировать это на Python, напрямую посылая сигнал самим себе:

import osimport signalfrom django.test import SimpleTestCaseclass FaulthandlerTests(SimpleTestCase):    def test_segv(self):        # Directly trigger the segmentation fault        # signal, which normally occurs due to        # unsafe memory access in C        os.kill(os.getpid(), signal.SIGSEGV)

Если мы делаем тест в Django 3.1, мы видим это:

$ ./manage.py test example.core.tests.test_faulthandlerSystem check identified no issues (0 silenced).[1]    31127 segmentation fault  ./manage.py test

Это нам не очень помогает, так как нет никакой зацепки на то, что вызвало ошибку сегментации.

Вместо этого мы видим трассировку на Django 3.2:

$ ./manage.py test example.core.tests.test_faulthandlerSystem check identified no issues (0 silenced).Fatal Python error: Segmentation faultCurrent thread 0x000000010ed1bdc0 (most recent call first):  File "/.../example/core/tests/test_faulthandler.py", line 12 in test_segv  File "/.../python3.9/unittest/case.py", line 550 in _callTestMethod  ...  File "/.../django/test/runner.py", line 668 in run_suite  ...  File "/..././manage.py", line 17 in main  File "/..././manage.py", line 21 in <module>[1]    31509 segmentation fault  ./manage.py test

( Сокращенно )

Faulthandler не может генерировать точно такую же трассировку, как стандартное исключение в Python, но он дает нам много информации для дибаггинга сбоя.

3. Timing (тайминг)

В примечании к релизу говорится:

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

Команда manage.py test включает опцию --timing, которая активирует несколько строк вывода в конце пробного тест-запуска для подведения итогов по настройке базы данных и времени:

$ ./manage.py test --timingCreating test database for alias 'default'...System check identified no issues (0 silenced)....----------------------------------------------------------------------Ran 3 tests in 0.002sOKDestroying test database for alias 'default'...Total database setup took 0.019s  Creating 'default' took 0.019sTotal database teardown took 0.000sTotal run took 0.028s

Благодарим Ахмада А. Хуссейна за участие в этом мероприятии в рамках Google Summer of Code 2020.

Если вы используете pytest, опция --durations N работает схоже.

Из-за системы фикстуры pytest время настройки базы данных будет отображаться как время настройки (setup time) только для одного теста, что сделает этот тест более медленным, чем он есть на самом деле.

4. Обратный вызов (callbacks) тестаtransaction.on_commit()

В примечании к релизу говорится:

Новый метод TestCase.captureOnCommitCallbacks() собирает функции обратного вызова (callbacks functions), переданные в transaction.on_commit(). Это позволяет вам тестировать эти callbacks, не используя при этом более медленный TransactionTestCase.

Это вклад, который я ранее сделал и о котором ранее рассказывал.

Итак, представьте, что вы используете опцию ATOMIC_REQUESTSот Django, чтобы перевести каждый вид в транзакцию (а я думаю, что так и должно быть!). Затем вам нужно использовать функцию transaction.on_commit() для выполнения любых действий, которые зависят от того, насколько длительно данные хранятся в базе данных. Например, в этом простом представлении для формы контактов:

from django.db import transactionfrom django.views.decorators.http import require_http_methodsfrom example.core.models import ContactAttempt@require_http_methods(("POST",))def contact(request):    message = request.POST.get('message', '')    attempt = ContactAttempt.objects.create(message=message)    @transaction.on_commit    def send_email():        send_contact_form_email(attempt)    return redirect('/contact/success/')

Мы не посылаем сообщения по электронной почте, если только транзакция не сохраняет изменения, так как в противном случае в базе данных не будет ContactAttempt.

Это правильный способ написания подобного вида, но ранее было сложно протестировать callback (обратный вызов), переданный функции on_commit(). Django не будет запускать callback без сохранения транзакции, а его TestCase избегает сохранения, и вместо этого откатывает (rollback) транзакцию, так что это повторяется при каждом тесте.

Одним из решений является использование TransactionTestCase, которое позволяет сохранять транзакции кода. Но это намного медленнее, чем TestCase , так как он очищает все таблицы между тестами.

(Я ранее говорил об увеличении скорости в три раза благодаря конвертации тестов из TransactionTestCase в TestCase.)

Решением в Django 3.2 является новая функция captureOnCommitCallbacks(), которую мы используем в качестве контекстного менеджера. Она захватывает любые callbacks и позволяет вам добавлять утверждения или проверять их эффект. Мы можем использовать это, чтобы проверить наше мнение таким образом:

from django.core import mailfrom django.test import TestCasefrom example.core.models import ContactAttemptclass ContactTests(TestCase):    def test_post(self):        with self.captureOnCommitCallbacks(execute=True) as callbacks:            response = self.client.post(                "/contact/",                {"message": "I like your site"},            )        assert response.status_code == 302        assert response["location"] == "/contact/success/"        assert ContactAttempt.objects.get().message == "I like your site"        assert len(callbacks) == 1        assert len(mail.outbox) == 1        assert mail.outbox[0].subject == "Contact Form"        assert mail.outbox[0].body == "I like your site"

Итак, мы используем captureOnCommitCallbacks() по запросу тестового клиента на просмотр, передавая execute флаг, чтобы указать, что фальшивое сохранение (коммит) должно запускать все callbacks. Затем мы проверяем HTTP-ответ и состояние базы данных, прежде чем проверить электронную почту, отправленную обратным вызовом (callback). Наш тест затем покрывает все на просмотре, оставаясь быстрым и классным!

Чтобы использовать captureOnCommitCallbacks() в ранних версиях Django, установите django-capture-on-commit-callbacks.

5. Улучшенный assertQuerysetEqual()

В примечании к релизу говорится:

TransactionTestCase.assertQuerysetEqual() в данный момент поддерживает прямое сравнение с другой выборкой элементов запроса в Django

Если вы используете assertQuerysetEqual() в ваших тестах, это изменение точно улучшит вашу жизнь!

В дополнение к Django 3.2, assertQuerysetEqual() требует от вас сравнения с QuerySet после трансформации. Далее происходит переход по умолчанию к repr(). Таким образом, тесты, использующие его, обычно проходят список предварительно вычисленных repr() strings для вышеупомянутого сравнения:

from django.test import TestCasefrom example.core.models import Bookclass AssertQuerySetEqualTests(TestCase):    def test_comparison(self):        Book.objects.create(title="Meditations")        Book.objects.create(title="Antifragile")        self.assertQuerysetEqual(            Book.objects.order_by("title"),            ["<Book: Antifragile>", "<Book: Meditations>"],        )

Проверка требует немного больше размышлений о том, какие экземпляры моделей ожидаются. А также требует отработки тестов в случае изменения метода repr() модели.

Из Django 3.2 можно передать QuerySet или список объектов для сравнения, что позволяет упростить тест:

from django.test import TestCasefrom example.core.models import Bookclass AssertQuerySetEqualTests(TestCase):    def test_comparison(self):        book1 = Book.objects.create(title="Meditations")        book2 = Book.objects.create(title="Antifragile")        self.assertQuerysetEqual(            Book.objects.order_by("title"),            [book2, book1],        )

Спасибо Питеру Инглсби и Хасану Рамезани за то, что они внесли эти изменения. Они помогли улучшить тестовый набор Django.

Финал

Наслаждайтесь этими изменениями, когда Django 3.2 выйдет или сделайте это раньше через пакет backport.


Перевод статьи подготовлен в преддверии старта курса Web-разработчик на Python.

Также приглашаем всех желающих посмотреть открытый вебинар на тему Использование сторонних библиотек в django. На занятии мы рассмотрим общие принципы установки и использования сторонних библиотек вместе с django; а также научимся пользоваться несколькими популярными библиотеками: django-debug-toolbar, django-cms, django-cleanup и т.д.

Источник: habr.com
К списку статей
Опубликовано: 01.03.2021 20:22:05
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании otus. онлайн-образование

Разработка веб-сайтов

Python

Django

Web-разработка

Категории

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

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru