DevGang
Авторизоваться

Введение в тестирование с помощью Django для Python

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

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

В этой статье мы увидим, как работают оба.

Давайте начнем!

Что мы будем тестировать?

Все тесты, которые мы будем наблюдать, будут использовать один и тот же код — конечную точку BookList.

Модель Book имеет поля titleauthor и date_published. Обратите внимание, что порядок по умолчанию устанавливается с помощью date_published.

models.py
class Book(models.Model):
    title = models.CharField(max_length=20)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, blank=True, null=True)
    date_published = models.DateField(null=True, blank=True)
 
    class Meta:
        ordering = ['date_published']

Просто представление ListView:

views.py
from django.views.generic import ListView
from .models import Book
 
 
class BookListView(ListView):
    model = Book

Поскольку тестовые примеры будут проверять конечную точку, нам также понадобится URL-адрес:

urls.py
from django.urls import path
from . import views
 
urlpatterns = [
    path("books/", views.BookListView.as_view(), name="book_list"),
]

Теперь о тестах.

Unittest для Python

Unittest — это встроенная среда тестирования Python. Django расширяет его некоторыми собственными функциями.

Поначалу вы можете запутаться в том, какие методы принадлежат unittest и каковы расширения Django.

Unittest обеспечивает:

  1. TestCase, выступающий в качестве базового класса.
  2. setUp() и tearDown() методы для кода, который вы хотите выполнить до или после каждого метода тестирования. setUpClass() и tearDownClass() также запускают один раз за целое TestClass.
  3. Несколько типов утверждений, таких как assertEqualassertAlmostEqual и assertIsInstance.

Основной частью среды тестирования Django является TestCase.

Поскольку он является подклассом unittest.TestCaseTestCase имеет все те же функциональные возможности, но также строится на его основе:

  1. Метод класса setUpTestData: данные создаются один раз для каждого теста TestCase (в отличие от метода setUp, который создает тестовые данные один раз для каждого теста). Использование setUpTestData может значительно ускорить ваши тесты.
  2. Тестовые данные загружаются через fixtures.
  3. Утверждения, специфичные для Django, такие как assertQuerySetEqualassertFormError и assertContains.
  4. Временные настройки переопределения, которые можно настроить во время тестового запуска.

Платформа тестирования Django также предоставляет клиент, который не зависит от TestCase и поэтому может использоваться с pytest. Client служит фиктивным браузером, позволяющим пользователям создавать GET и POST запросы.

Как создаются тесты с помощью Unittest

Unittest поддерживает обнаружение тестов, но необходимо соблюдать следующие правила:

  1. Тест должен находиться в файле с префиксом test.
  2. Тест должен находиться внутри класса, который является подклассом unittest.TestCase (включая использование django.TestCase). Включение «Test» в имя класса не является обязательным.
  3. Имя метода тестирования должно начинаться с test_.

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

tests.py
from datetime import date
 
from django.test import TestCase
from django.urls import reverse
from .models import Book, Author
 
class BookListTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        author = Author.objects.create(first_name='Jane', last_name='Austen')
        cls.second_book = Book.objects.create(title='Emma', author=author, date_published=date(1815, 1, 1))
        cls.first_book = Book.objects.create(title='Pride and Prejudice', author=author, date_published=date(1813, 1, 1))
 
    def test_book_list_returns_all_books(self):
        response = self.client.get(reverse('book_list'))
        response_book_list = response.context['book_list']
 
        self.assertEqual(response.status_code, 200)
        self.assertIn(self.first_book, response_book_list)
        self.assertIn(self.second_book, response_book_list)
        self.assertEqual(len(response_book_list), 2)
 
    def test_book_list_ordered_by_date_published(self):
        response = self.client.get(reverse('book_list'))
        books = list(response.context['book_list'])
 
        self.assertEqual(response.status_code, 200)
        self.assertQuerySetEqual(books, [self.first_book, self.second_book])

Вы всегда должны помещать свои тестовые функции внутри класса, который расширяет класс TestCase. Хотя это и не обязательно, мы включили слово «Test» в имя нашего класса, чтобы с первого взгляда его можно было легко узнать как тестовый класс. Обратите внимание, что файл TestCase импортируется из Django, а не из unittest — вам нужен доступ к дополнительным функциям Django.

Что тут происходит?

Целью этого TestCase является проверка правильности работы конечной точки book_list. Существует два теста: один проверяет, что все объекты Book в базе данных включены в ответ как book_list, а второй проверяет, что книги перечислены по возрастанию даты публикации.

Хотя тесты кажутся похожими, они оценивают две разные функции, которые не следует объединять в один тест.

Поскольку обоим тестам нужны одни и те же данные в базе данных, для подготовки данных мы используем метод класса setUpTestData — оба теста будут иметь к ним доступ, не повторяя процесс дважды. В отличие от двух объектов Book, этот объект Author не нужен вне метода установки, поэтому мы не устанавливаем его как атрибут экземпляра. Я поменял порядок двух книг, чтобы гарантировать, что правильный порядок не является случайным.

Эти два теста выглядят очень похоже — они оба отправляют запрос GET на один и тот же URL-адрес и извлекают файл book_list из context. Методы внутри класса TestCase автоматически имеют доступ к client, который используется для выполнения запроса.

До этого момента оба теста делали одно и то же, но утверждения различались.

Первый тест утверждает, что обе книги находятся в контексте ответа, используя assertIn. Это также обеспечивает длину объекта response.context['book_list']. Во втором тесте используется Django assertQuerySetEqual. Это утверждение проверяет порядок по умолчанию, поэтому оно делает именно то, что нам нужно.

Оба теста также проверяют status_code, поэтому вы сразу узнаете, если URL-адрес не работает.

Запуск нашего теста с помощью Unittest

Мы можем запустить тест с помощью тестовой команды Django:

(venv)$ python manage.py test

Это результат:

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

Что, если мы проведем неудачный тест? Например, если порядок возвращаемых объектов не соответствует желаемому результату:

(venv)$ python manage.py test
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_book_order (bookstore.test.BookListTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/bookstore/test.py", line 17, in test_book_order
    self.assertEqual(books, [self.second_book, self.first_book])
AssertionError: Lists differ: [<Book: Pride and Prejudice>, <Book: Emma>] != [<Book: Emma>, <Book: Pride and Prejudice>]
 
First differing element 0:
<Book: Pride and Prejudice>
<Book: Emma>
 
- [<Book: Pride and Prejudice>, <Book: Emma>]
+ [<Book: Emma>, <Book: Pride and Prejudice>]
----------------------------------------------------------------------
Ran 2 tests in 0.085s
 
FAILED (failures=1)
Destroying test database for alias 'default'...

Как видите, вывод весьма информативен — вы узнаете, какой тест не пройден, в какой строке и почему. Тест не удался, поскольку списки различаются; вы даже можете просмотреть оба списка, чтобы быстро определить, в чем заключается проблема.

Еще одна важная вещь, на которую следует обратить внимание, это то, что только второй тест не удался — выяснить, что не так, может быть сложнее, если есть один тест.

Pytest для Python

Pytest — отличная альтернатива unittest. Несмотря на то, что он не является встроенным Python, он считается более питоническим, чем unittest. Он не требует TestClass, имеет меньше шаблонного кода и имеет простой оператор assert. Pytest имеет богатую экосистему плагинов, включая специальный плагин Django, pytest-django.

Подход pytest сильно отличается от unittest.

Среди прочего:

  1. Вы можете писать тесты на основе функций без необходимости использования классов.
  2. Он позволяет создавать приспособления: компоненты многократного использования для установки и разборки. Фикстуры могут иметь разные области действия (например, module), что позволяет оптимизировать производительность, сохраняя при этом изолированность теста.
  3. Параметризация тестовой функции означает, что ее можно эффективно запускать с разными аргументами.
  4. У него есть одно простое утверждение assert.

Pytest и Django

pytest-django служит адаптером между Pytest и Django. Тестирование Django с помощью pytest без pytest-django технически возможно, но непрактично и не рекомендуется. Помимо обработки настроек, статических файлов и шаблонов Django, он добавляет в pytest некоторые инструменты тестирования Django, а также добавляет свои собственные:

  1. Декоратор @pytest.mark.django_db, обеспечивающий доступ к базе данных для определенного теста.
  2. client, который передает Django Client в виде фикстуры.
  3. Устройство admin_client возвращает аутентифицированный доступ Client с правами администратора.
  4. Фикстура settings, позволяющая временно отменять настройки Django во время теста.
  5. Те же специальные утверждения Django, что и Django TestCase.

Как создаются тесты с помощью Pytest

Как и unittest, pytest также поддерживает обнаружение тестов. Как вы увидите ниже, стандартные правила (эти правила можно легко изменить):

  1. Файлы должны называться test_*.py или *_test.py.
  2. Тестовые классы не нужны, но если вы хотите их использовать, имя должно иметь префикс Test.
  3. Имена функций должны начинаться с префикса test.

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

from datetime import date
 
import pytest
from django.urls import reverse
 
from bookstore.models import Author, Book
 
 
@pytest.mark.django_db
def test_book_list_returns_all_books(client):
    author = Author.objects.create(first_name='Jane', last_name='Austen')
    second_book = Book.objects.create(title='Emma', author=author, date_published=date(1815, 1, 1))
    first_book = Book.objects.create(title='Pride and Prejudice', author=author, date_published=date(1813, 1, 1))
 
    response = client.get(reverse('book_list'))
    books = list(response.context['book_list'])
 
    assert response.status_code == 200
    assert first_book in books
    assert second_book in books
    assert len(books) == 2
 
 
@pytest.mark.django_db
def test_book_list_ordered_by_date_published(client):
    author = Author.objects.create(first_name='Jane', last_name='Austen')
    second_book = Book.objects.create(title='Emma', author=author, date_published=date(1815, 1, 1))
    first_book = Book.objects.create(title='Pride and Prejudice', author=author, date_published=date(1813, 1, 1))
 
    response = client.get(reverse('book_list'))
    books = list(response.context['book_list'])
 
    assert response.status_code == 200
    assert books == [first_book, second_book]

Что тут происходит?

Эти два теста оценивают ту же функциональность, что и два предыдущих модульных теста: первый тест гарантирует, что все книги в базе данных возвращены, а второй гарантирует, что книги упорядочены по дате публикации. Более подробную информацию о модульных тестах вы можете узнать в нашей статье "Профессиональные модульные тесты на Python".

Итак, чем же они отличаются?

  1. Они полностью автономны — классная или совместная подготовка данных не требуется. Вы можете поместить их в два разных файла, и ничего не изменится.
  2. Поскольку им нужен доступ к базе данных, им требуется декоратор @pytest.mark.django_db из pytest-django. Если доступ к БД не требуется, пропустите декоратор.
  3. Django client необходимо передавать как приспособление, поскольку у вас нет автоматического доступа к нему. Опять же, это исходит от pytest-django.
  4. Вместо различных утверждений assert используется простой.

Строки, выполняющие запрос и возвращающие ответ book_list, точно такие же, как и в unittest.

Фикстуры в Pytest

В pytest нет setUpTestData. Но если вы обнаружите, что повторяете один и тот же код подготовки данных снова и снова, вы можете использовать фикстуру.

В нашем примере приспособление будет выглядеть так:

fixture creation
import pytest
 
@pytest.fixture
def two_book_objects_same_author():
    author = Author.objects.create(first_name='Jane', last_name='Austen')
    second_book = Book.objects.create(title='Emma', author=author, date_published=date(1815, 1, 1))
    first_book = Book.objects.create(title='Pride and Prejudice', author=author, date_published=date(1813, 1, 1))
 
    return [first_book, second_book]
 
# fixture usage
@pytest.mark.django_db
def test_book_list_ordered_by_date_published_with_fixture(client, two_book_objects_same_author):
    response = client.get(reverse('book_list'))
    books = list(response.context['book_list'])
 
    assert response.status_code == 200
    assert books == two_book_objects_same_author

Вы отмечаете фикстуру с помощью декоратора @pytest.fixture. Затем вам нужно передать его в качестве аргумента для использования функции. Вы можете использовать фикстуры, чтобы избежать повторяющегося кода на этапе подготовки или улучшить читаемость, но не переусердствуйте.

Запуск теста с помощью Pytest для Django

Чтобы pytest работал правильно, вам нужно сообщить django-pytest, где можно найти настройки Django. Хотя это можно сделать в терминале (pytest --ds=bookstore.settings), лучше использовать pytest.ini.

В то же время вы можете использовать pytest.ini для предоставления других параметров конфигурации.

pytest.ini выглядит примерно так:

[pytest]
 
;where the django settings are
DJANGO_SETTINGS_MODULE = bookstore.settings
 
;changing test discovery
python_files = tests.py test_*.py
 
;output logging records into the console
log_cli = True

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

(venv)$ pytest

Давайте посмотрим результат:

================================================================================================= test session starts ==================================================================================================
platform darwin -- Python 3.10.5, pytest-7.4.3, pluggy-1.3.0
django: settings: bookstore.settings (from option)
rootdir: /bookstore
plugins: django-4.5.2
collected 2 items
 
bookstore/test_with_pytest.py::test_book_list_returns_all_books_with_pytest PASSED                                                                                                                                   [ 50%]
bookstore/test_with_pytest.py::test_book_list_ordered_by_date_published_with_pytest PASSED                                                                                                                           [100%]
 
================================================================================================== 2 passed in 0.58s ===================================================================================================

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

И как будет выглядеть результат, если тест не пройден?

================================================================================================= test session starts ==================================================================================================
platform darwin -- Python 3.10.5, pytest-7.4.3, pluggy-1.3.0
django: settings: bookstore.settings (from ini)
rootdir: /bookstore
configfile: pytest.ini
plugins: django-4.5.2
collected 2 items

bookstore/test_with_pytest.py::test_book_list_returns_all_books_with_pytest PASSED                                                                                                                                   [ 50%]
bookstore/test_with_pytest.py::test_book_list_ordered_by_date_published_with_pytest FAILED                                                                                                                           [100%]

======================================================================================================= FAILURES =======================================================================================================
___________________________________________________________________________________________________ test_book_order ____________________________________________________________________________________________________

client = <django.test.client.Client object at 0x1064dbfa0>, books_in_correct_order = [<Book: Emma>, <Book: Pride and Prejudice>]

    @pytest.mark.django_db
    def test_book_order(client, books_in_correct_order):
        response = client.get(reverse('book_list'))
        books = list(response.context['book_list'])

>       assert books == books_in_correct_order
E       assert [<Book: Pride... <Book: Emma>] == [<Book: Emma>...nd Prejudice>]
E         At index 0 diff: <Book: Pride and Prejudice> != <Book: Emma>
E         Full diff:
E         - [<Book: Emma>, <Book: Pride and Prejudice>]
E         + [<Book: Pride and Prejudice>, <Book: Emma>]

=============================================================================================== short test summary info ================================================================================================
FAILED bookstore/test_with_pytest.py::test_book_list_ordered_by_date_published_with_pytest - assert [<Book: Pride... <Book: Emma>] == [<Book: Emma>...nd Prejudice>]
============================================================================================= 1 failed, 1 passed in 0.78s ==============================================================================================

Если log_cli установлено значение True, вы можете легко увидеть, какой тест пройден, а какой нет. Несмотря на то, что сообщение об ошибке содержит длинный отчет, вы часто можете найти достаточно информации о том, почему тест не пройден, в краткой сводной информации о тесте.

Unittest против Pytest для Python

Нет единого мнения относительно того, что лучше: unittest или pytest. Мнения разработчиков расходятся, в чем можно убедиться на StackOverflow или Reddit. Многое зависит от ваших собственных предпочтений или предпочтений вашей команды. Если большая часть вашей команды привыкла к pytest, нет необходимости переключаться, и наоборот.

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

unittest (с тестом Django) pytest (с pytest-django)
Никакой дополнительной установки не требуется Требуется установка pytest и pytest-django.
Классовый подход Функциональный подход
Несколько типов утверждений Простое assert заявление
Методы установки/разрыва внутри TestCase класса Система креплений с различными возможностями
Параметризация не поддерживается (но ее можно выполнить самостоятельно) Встроенная поддержка параметризации

Как подходить к тестированию в Django

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

Одна вещь, которая может помочь вам покрыть большую часть вашего кода тестами, — это покрытие кода. « Покрытие кода » — это показатель, показывающий, какой процент кода вашей программы выполняется во время выполнения набора тестов. Чем выше процент, тем большую часть тестируемого кода обычно означает меньшее количество незамеченных проблем.

Coverage.py — это инструмент для измерения покрытия кода программ Python. После установки вы можете использовать его либо с unittest, либо с pytest.

Если вы используете pytest, вы можете установить pytest-cov — библиотеку, которая интегрирует Coverage.py с pytest. Он широко используется, но в официальной документации Coverage.py указано, что он по большей части не нужен.

Команды для запуска покрытия для unittest и pytest немного отличаются:

  1. Для unittest: coverage run manage.py test
  2. Для pytest: coverage run -m pytest

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

Чтобы увидеть отчет в вашем терминале, вам нужно выполнить coverage report команду:

$(venv) coverage report
Name                                                                             Stmts   Miss  Cover
------------------------------------------------------------------------------------------------------
core/__init__.py                                                         0      0    100%
core/settings.py                                                         34     0    100%
core/urls.py                                                             3      0    100%
bookstore/__init__.py                                                             0      0    100%
bookstore/admin.py                                                                19     1    95%
bookstore/apps.py                                                                 6      0    100%
bookstore/models.py                                                               64     7    89%
bookstore/urls.py                                                                 3      0    100%
bookstore/views.py                                                                34     7    79%
------------------------------------------------------------------------------------------------------
TOTAL                                                                             221     15    93%

Проценты в диапазоне от 75% до 100% считаются «хорошим охватом». Стремитесь к результату выше 75 %, но имейте в виду, что если ваш код хорошо покрыт тестами, это еще не значит, что он хорош.

Примечание о тестах

Тестовое покрытие — один из аспектов хорошего набора тестов. Однако, если ваш охват не ниже 75%, точный процент не так уж важен.

Не пишите тесты просто ради их написания или для повышения процента покрытия. Не все в Django нуждается в тестировании — написание бесполезных тестов замедлит работу вашего набора тестов и сделает рефакторинг медленным и болезненным процессом.

Не следует тестировать собственный код Django — он уже протестирован. Например, вам не нужно писать тест, который проверяет, получен ли объект с помощью get_object_or_404В наборе тестов Django это уже предусмотрено.

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

Как AppSignal может помочь в тестировании в Django?

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

Вот почему рекомендуется использовать мониторинг приложений — он может помочь вам отслеживать ошибки и контролировать производительность. Самое главное, он может сообщить вам, когда ошибка является фатальной и нарушает работу вашего приложения.

AppSignal интегрируется с Django и может помочь вам найти ошибки, которые вы упускаете при написании тестов.

С помощью AppSignal вы можете увидеть, как часто и когда возникает ошибка. Знание этого может помочь вам решить, насколько срочно вам нужно исправить ошибку.

Поскольку вы можете интегрировать AppSignal со своим репозиторием Git, вы также можете точно определить строку, в которой возникает ошибка. Это упрощает процесс определения причины ошибки и облегчает ее исправление. Когда ошибка хорошо понята, легче добавить тесты, которые предотвратят ее и подобные ошибки в будущем.

Вот пример ошибки из списка «Проблемы» на панели «Ошибки» для приложения Django в AppSignal:

Подведение итогов

В этом посте мы увидели, что у вас есть два отличных варианта тестирования в Django: unittest и pytest. Мы представили оба варианта, подчеркнув их уникальные сильные стороны и возможности. Какой из них вы выберете, во многом зависит от ваших личных предпочтений или предпочтений конкретного проекта.

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

Приятного кодирования!

Источник:

#Python #Django #Testing
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу