[Разработка веб-сайтов, Python, Django] Новое тестирование фичей в Django 3.2 (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Пару недель назад Django 3.2 выпустил свой первый альфа-релиз, а финальный релиз выйдет в апреле. Он содержит микс новых возможностей, о которых вы можете прочитать в примечаниях к релизу. Эта статья посвящена изменениям в тестировании, некоторые из которых можно получить на более ранних версиях Django с пакетами backport.1. Изоляция setUpTestData() В примечании к релизу говорится:«Объекты, назначенные для классификации атрибутов в TestCase.setUpTestData() теперь выделяются для каждого тестового метода».setUpTestData() — очень полезный прием для быстрого выполнения тестов, и это изменение делает его использование намного проще.Прием TestCase.setUp() нередко используется из юнит-теста для создания экземпляров моделей, которые используются в каждом тесте:
from django.test import TestCase
from example.core.models import Book
class ExampleTests(TestCase):
def setUp(self):
self.book = Book.objects.create(title="Meditations")
Тест-раннер вызывает setUp() перед каждым тестом. Это дает простую изоляцию тестов, так как берутся свежие данные для каждого теста. Недостатком такого подхода является то, что setUp() запускается много раз, что при большом объеме данных или тестов может происходить довольно медленно.setUpTestData() позволяет создавать данные на уровне класса — один раз на TestCase. Его использование очень похоже на setUp(), только это метод класса:
from django.test import TestCase
from example.core.models import Book
class ExampleTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.book = Book.objects.create(title="Meditations")
В промежутках между тестами Django продолжает откатывать (rollback) любые изменения в базе данных, поэтому они остаются там изолированными. К сожалению, до этого изменения в Django 3.2 rollback не происходили в памяти, поэтому любые изменения в экземплярах моделей сохранялись. Это означает, что тесты не были полностью изолированы.Возьмем, к примеру, эти тесты:
from django.test import TestCase
from example.core.models import Book
class 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_setuptestdata
Creating 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.002s
FAILED (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_setuptestdata
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.002s
OK
Destroying 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 os
import signal
from django.test import SimpleTestCase
class 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_faulthandler
System check identified no issues (0 silenced).
[1] 31127 segmentation fault ./manage.py test
Это нам не очень помогает, так как нет никакой зацепки на то, что вызвало ошибку сегментации.Вместо этого мы видим трассировку на Django 3.2:
$ ./manage.py test example.core.tests.test_faulthandler
System check identified no issues (0 silenced).
Fatal Python error: Segmentation fault
Current 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 --timing
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.002s
OK
Destroying test database for alias 'default'...
Total database setup took 0.019s
Creating 'default' took 0.019s
Total database teardown took 0.000s
Total 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 transaction
from django.views.decorators.http import require_http_methods
from 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 mail
from django.test import TestCase
from example.core.models import ContactAttempt
class 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 TestCase
from example.core.models import Book
class 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 TestCase
from example.core.models import Book
class 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
===========
===========
Автор оригинала: Adam Johnson
===========Похожие новости:
- [SQL] Секционирование таблиц и время компиляции плана запроса в SQL Server (перевод)
- [Open source, OpenStreetMap, Открытые данные, Визуализация данных] День открытых данных 2021. Онлайн
- [Python, Машинное обучение, Искусственный интеллект, TensorFlow] Нейродайджест: главное из области машинного обучения за февраль 2021
- [Python, Django] Архитектура в Django проектах — как выжить
- [Разработка веб-сайтов, Разработка мобильных приложений] 10 инструментов для разработки, которые вам стоит попробовать (перевод)
- [Python] Облегчаем себе жизнь с помощью BeautifulSoup4
- [Python, Алгоритмы, Карьера в IT-индустрии, Natural Language Processing] Как улучшить резюме с помощью алгоритмов обработки текстов на естественных языках (перевод)
- [Микросервисы] Микросервисы и безопасность (перевод)
- [Тестирование IT-систем, Java] Краткое сравнение JUnit и TestNG (перевод)
- [Python, Программирование] Пишем телеграм-бота, который будет переводить интернет статьи в mp3-файлы
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_python, #_django, #_django, #_python, #_webrazrabotka (web-разработка), #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
), #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_python, #_django
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:11
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Пару недель назад Django 3.2 выпустил свой первый альфа-релиз, а финальный релиз выйдет в апреле. Он содержит микс новых возможностей, о которых вы можете прочитать в примечаниях к релизу. Эта статья посвящена изменениям в тестировании, некоторые из которых можно получить на более ранних версиях Django с пакетами backport.1. Изоляция setUpTestData() В примечании к релизу говорится:«Объекты, назначенные для классификации атрибутов в TestCase.setUpTestData() теперь выделяются для каждого тестового метода».setUpTestData() — очень полезный прием для быстрого выполнения тестов, и это изменение делает его использование намного проще.Прием TestCase.setUp() нередко используется из юнит-теста для создания экземпляров моделей, которые используются в каждом тесте: from django.test import TestCase
from example.core.models import Book class ExampleTests(TestCase): def setUp(self): self.book = Book.objects.create(title="Meditations") from django.test import TestCase
from example.core.models import Book class ExampleTests(TestCase): @classmethod def setUpTestData(cls): cls.book = Book.objects.create(title="Meditations") from django.test import TestCase
from example.core.models import Book class 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" $ ./manage.py test example.core.tests.test_setuptestdata
Creating 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.002s FAILED (failures=1) Destroying test database for alias 'default'... $ ./manage.py test example.core.tests.test_setuptestdata
Creating test database for alias 'default'... System check identified no issues (0 silenced). ... ---------------------------------------------------------------------- Ran 3 tests in 0.002s OK Destroying test database for alias 'default'... DiscoverRunner сейчас использует faulthandler по умолчанию.
import os
import signal from django.test import SimpleTestCase class 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) $ ./manage.py test example.core.tests.test_faulthandler
System check identified no issues (0 silenced). [1] 31127 segmentation fault ./manage.py test $ ./manage.py test example.core.tests.test_faulthandler
System check identified no issues (0 silenced). Fatal Python error: Segmentation fault Current 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 DiscoverRunner теперь может трекать тайминг, включая настройку базы данных и общее время работы.
$ ./manage.py test --timing
Creating test database for alias 'default'... System check identified no issues (0 silenced). ... ---------------------------------------------------------------------- Ran 3 tests in 0.002s OK Destroying test database for alias 'default'... Total database setup took 0.019s Creating 'default' took 0.019s Total database teardown took 0.000s Total run took 0.028s Новый метод TestCase.captureOnCommitCallbacks() собирает функции обратного вызова (callbacks functions), переданные в transaction.on_commit(). Это позволяет вам тестировать эти callbacks, не используя при этом более медленный TransactionTestCase.
from django.db import transaction
from django.views.decorators.http import require_http_methods from 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/') from django.core import mail
from django.test import TestCase from example.core.models import ContactAttempt class 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" TransactionTestCase.assertQuerysetEqual() в данный момент поддерживает прямое сравнение с другой выборкой элементов запроса в Django
from django.test import TestCase
from example.core.models import Book class 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>"], ) from django.test import TestCase
from example.core.models import Book class 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], ) Перевод статьи подготовлен в преддверии старта курса «Web-разработчик на Python».Также приглашаем всех желающих посмотреть открытый вебинар на тему «Использование сторонних библиотек в django». На занятии мы рассмотрим общие принципы установки и использования сторонних библиотек вместе с django; а также научимся пользоваться несколькими популярными библиотеками: django-debug-toolbar, django-cms, django-cleanup и т.д.
=========== Источник: habr.com =========== =========== Автор оригинала: Adam Johnson ===========Похожие новости:
Блог компании OTUS. Онлайн-образование ), #_razrabotka_vebsajtov ( Разработка веб-сайтов ), #_python, #_django |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:11
Часовой пояс: UTC + 5