Spec-Zone.ru › Django 4.2

Инструменты тестирования

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

Клиент тестирования

Клиент тестирования — это класс Python, который действует как эмуляция веб-браузера, позволяющий тестировать ваши представления и взаимодействовать с вашим приложением Django программно.

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

  • Имитация запросов GET и POST к URL и наблюдение за ответом — от низкоуровневого HTTP (заголовки результатов и коды состояния) до содержимого страницы.
  • Просмотр цепочки редиректов (если таковые имеются) и проверка URL и кода состояния на каждом шаге.
  • Проверка того, что заданный запрос отображается заданным шаблоном Django с контекстом шаблона, содержащим определенные значения.

Обратите внимание, что клиент тестирования не предназначен для замены Selenium или других фреймворков «в браузере». Клиент тестирования Django имеет другую направленность. Короче говоря:

  • Используйте клиент тестирования Django, чтобы убедиться, что отображается правильный шаблон и что шаблон получает правильные данные контекста.
  • Используйте RequestFactory для прямого тестирования функций представлений, минуя маршрутизацию и слои промежуточного ПО.
  • Используйте фреймворки в браузере, такие как Selenium, для тестирования отображенного HTML и поведения веб-страниц, а именно функциональности JavaScript. Django также предоставляет специальную поддержку для этих фреймворков; см. раздел о LiveServerTestCase для получения дополнительной информации.

Комплектный набор тестов должен использовать комбинацию всех этих типов тестов.

Обзор и быстрый пример

Для использования клиента тестирования, создайте экземпляр django.test.Client и получите веб-страницы:

>>> from django.test import Client
>>> c = Client()
>>> response = c.post("/login/", {"username": "john", "password": "smith"})
>>> response.status_code
200
>>> response = c.get("/customer/details/")
>>> response.content
b'<!DOCTYPE html...'

Как показывает этот пример, вы можете создать экземпляр Client внутри сессии интерпретатора Python.

Обратите внимание на несколько важных моментов работы клиента тестирования:

  • Клиент тестирования не требует запуска веб-сервера. На самом деле, он будет работать без запуска веб-сервера! Это происходит потому, что он избегает накладных расходов HTTP и работает напрямую с фреймворком Django. Это помогает ускорить выполнение модульных тестов.
  • При получении страниц не забудьте указать путь URL, а не весь домен. Например, это правильно:

    >>> c.get("/login/")
    

    Это неправильно:

    >>> c.get("https://www.example.com/login/")
    

    Клиент тестирования не может получать веб-страницы, которые не работают с вашим проектом Django. Если вам нужно получить другие веб-страницы, используйте модуль стандартной библиотеки Python, такой как urllib.

  • Для разрешения URL-адресов клиент тестирования использует конфигурацию URLconf, на которую указывает ваш параметр ROOT_URLCONF.
  • Хотя приведенный выше пример сработает в интерактивном интерпретаторе Python, некоторые функциональные возможности клиента тестирования, в частности связанные с шаблонами, доступны только во время выполнения тестов.

    Причина в том, что запуск тестов Django выполняет некоторую «чёрную магию», чтобы определить, какой шаблон загружен данным представлением. Эта «чёрная магия» (по существу, исправление системы шаблонов Django в памяти) происходит только во время выполнения тестов.

  • По умолчанию клиент тестирования отключит все проверки CSRF, выполняемые вашим сайтом.

    Если по какой-то причине вам нужны проверки CSRF с помощью клиента тестирования, можно создать экземпляр клиента тестирования, который их выполняет. Для этого передайте аргумент enforce_csrf_checks при создании вашего клиента:

    >>> from django.test import Client
    >>> csrf_client = Client(enforce_csrf_checks=True)
    

Выполнение запросов

Используйте класс django.test.Client для выполнения запросов.

class Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, *, headers=None, **defaults)

Тестовый HTTP-клиент. Принимает несколько аргументов, которые могут настроить поведение.

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

client = Client(headers={"user-agent": "curl/7.79.1"})

Произвольные ключевые аргументы в **defaults устанавливают переменные окружения WSGI переменные окружения. Например, чтобы установить имя скрипта:

client = Client(SCRIPT_NAME="/app/")

Примечание

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

Значения из ключевых аргументов headers и extra, переданных в get(), post() и т. д., имеют приоритет над значениями по умолчанию, переданными в конструктор класса.

Аргумент enforce_csrf_checks можно использовать для тестирования защиты от CSRF (см. выше).

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

Аргумент json_encoder позволяет задать пользовательский кодировщик JSON для сериализации JSON, о которой рассказано в post().

После получения экземпляра Client, можно вызвать любой из следующих методов:

Изменено в Django 4.2:

Добавлен параметр headers.

get(path, data=None, follow=False, secure=False, *, headers=None, **extra)

Выполняет запрос GET по указанному path и возвращает объект Response, описание которого приведено ниже.

Пары ключ-значение в словаре data используются для создания полезной нагрузки данных GET. Например:

>>> c = Client()
>>> c.get("/customers/details/", {"name": "fred", "age": 7})

…приведет к выполнению запроса GET, эквивалентного:

/customers/details/?name=fred&age=7

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

>>> c = Client()
>>> c.get(
...     "/customers/details/",
...     {"name": "fred", "age": 7},
...     headers={"accept": "application/json"},
... )

…отправит HTTP-заголовок HTTP_ACCEPT в представление деталей, что является хорошим способом тестирования путей кода, использующих метод django.http.HttpRequest.accepts().

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

>>> c = Client()
>>> c.get("/", SCRIPT_NAME="/app/")

Если у вас уже есть аргументы GET в кодировке URL, вы можете использовать эту кодировку вместо аргумента data. Например, предыдущий запрос GET также можно отправить как:

>>> c = Client()
>>> c.get("/customers/details/?name=fred&age=7")

Если вы предоставите URL с закодированными данными GET и аргументом data, аргумент data будет иметь приоритет.

Если вы установите follow в True, клиент будет следовать всем перенаправлениям, и в объекте ответа будет установлен атрибут redirect_chain, содержащий кортежи промежуточных URL и кодов состояния.

Если у вас был URL /redirect_me/, который перенаправлял на /next/, а тот, в свою очередь, на /final/, вы увидите:

>>> response = c.get("/redirect_me/", follow=True)
>>> response.redirect_chain
[('http://testserver/next/', 302), ('http://testserver/final/', 302)]

Если вы установите secure в True, клиент будет эмулировать запрос HTTPS.

Изменено в Django 4.2:

Добавлен параметр headers.

post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, **extra)

Выполняет запрос POST по указанному path и возвращает объект Response, описание которого приведено ниже.

Пары ключ-значение в словаре data используются для отправки данных POST. Например:

>>> c = Client()
>>> c.post("/login/", {"name": "fred", "passwd": "secret"})

…приведет к выполнению запроса POST по этому URL:

/login/

…с этими данными POST:

name=fred&passwd=secret

Если вы укажете content_type как application/json, то data сериализуется с помощью json.dumps(), если это словарь, список или кортеж. По умолчанию сериализация выполняется с помощью DjangoJSONEncoder, и может быть переопределена путём предоставления аргумента json_encoder в Client. Такая сериализация также происходит для запросов put(), patch() и delete().

Если вы укажете любой другой content_type (например, text/xml для XML-данных), содержимое data отправляется как есть в запросе POST, используя content_type в HTTP-заголовке Content-Type.

Если вы не укажете значение для content_type, значения в data будут переданы с типом содержимого multipart/form-data. В этом случае пары ключ-значение в data будут закодированы как multipart-сообщение и использованы для создания полезной нагрузки данных POST.

Для отправки нескольких значений для заданного ключа — например, для указания значений для <select multiple> — предоставьте значения в виде списка или кортежа для требуемого ключа. Например, это значение data отправит три выбранных значения для поля с именем choices:

{"choices": ["a", "b", "d"]}

Отправка файлов — это особый случай. Для отправки файла вам нужно только указать имя поля файла в качестве ключа и дескриптор файла, который вы хотите загрузить, как значение. Например, если у вашей формы есть поля name и attachment, последнее — FileField:

>>> c = Client()
>>> with open("wishlist.doc", "rb") as fp:
...     c.post("/customers/wishes/", {"name": "fred", "attachment": fp})
...

Вы также можете предоставить любой объект типа файла (например, StringIO или BytesIO) как дескриптор файла. Если вы загружаете в ImageField, объекту нужен атрибут name, который передаёт валидатор validate_image_file_extension. Например:

>>> from io import BytesIO
>>> img = BytesIO(
...     b"GIF89a\x01\x00\x01\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00"
...     b"\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x01\x00\x00"
... )
>>> img.name = "myimage.gif"

Обратите внимание, что если вы хотите использовать один и тот же дескриптор файла для нескольких вызовов post() , то вам нужно будет вручную сбросить указатель файла между отправками. Самый простой способ сделать это — вручную закрыть файл после того, как он был предоставлен post(), как показано выше.

Также убедитесь, что файл открыт таким образом, чтобы данные можно было прочитать. Если ваш файл содержит двоичные данные, такие как изображение, это означает, что вам нужно открыть файл в rb (режим чтения двоичных данных).

Параметры headers и extra работают так же, как и для Client.get().

Если в URL, который вы запрашиваете с помощью POST, содержатся закодированные параметры, эти параметры будут доступны в данных request.GET. Например, если вы выполните запрос:

>>> c.post("/login/?visitor=true", {"name": "fred", "passwd": "secret"})

… обработчик представления этого запроса может получить имя пользователя и пароль из request.POST и определить, был ли пользователь гостем, используя request.GET.

Если вы установите follow в True, клиент будет следовать всем перенаправлениям, и в объекте ответа будет установлен атрибут redirect_chain, содержащий кортежи промежуточных URL и кодов состояния.

Если вы установите secure в True, клиент будет эмулировать запрос HTTPS.

Изменено в Django 4.2:

Добавлен параметр headers.

head(path, data=None, follow=False, secure=False, *, headers=None, **extra)

Выполняет запрос HEAD по указанному path и возвращает объект Response. Этот метод работает так же, как Client.get(), включая параметры follow, secure, headers и extra, за исключением того, что он не возвращает тело сообщения.

Изменено в Django 4.2:

Добавлен параметр headers.

options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)

Выполняет запрос OPTIONS по указанному path и возвращает объект Response. Полезно для тестирования RESTful-интерфейсов.

Если предоставлен параметр data, он используется в качестве тела запроса, и заголовок Content-Type устанавливается в content_type.

Параметры follow, secure, headers и extra действуют так же, как для Client.get().

Изменено в Django 4.2:

Добавлен параметр headers.

put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)

Выполняет запрос PUT по указанному path и возвращает объект Response. Полезно для тестирования RESTful-интерфейсов.

Если предоставлен параметр data, он используется в качестве тела запроса, и заголовок Content-Type устанавливается в content_type.

Параметры follow, secure, headers и extra действуют так же, как и для Client.get().

Изменено в Django 4.2:

Добавлен параметр headers.

patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)

Выполняет запрос PATCH по указанному path и возвращает объект Response. Полезно для тестирования RESTful интерфейсов.

Параметры follow, secure, headers, и extra работают так же, как и для Client.get().

Изменено в Django 4.2:

Добавлен параметр headers.

delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)

Выполняет запрос DELETE по указанному path и возвращает объект Response. Полезно для тестирования RESTful интерфейсов.

Если предоставлен параметр data, он используется в качестве тела запроса, и устанавливается заголовок Content-Type со значением content_type.

Параметры follow, secure, headers, и extra работают так же, как и для Client.get().

Изменено в Django 4.2:

Добавлен параметр headers.

trace(path, follow=False, secure=False, *, headers=None, **extra)

Выполняет запрос TRACE по указанному path и возвращает объект Response. Полезно для моделирования диагностических зондирований.

В отличие от других методов запросов, data не предоставляется как ключевой параметр, чтобы соответствовать RFC 9110#section-9.3.8, который предписывает, что запросы TRACE не должны иметь тело.

Параметры follow, secure, headers, и extra работают так же, как и для Client.get().

Изменено в Django 4.2:

Добавлен параметр headers.

login(**credentials)

Если ваш сайт использует систему аутентификации Django, вы можете использовать метод login() тестового клиента для моделирования входа пользователя на сайт.

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

Формат аргумента credentials зависит от используемого обработчика аутентификации (настраивается параметром AUTHENTICATION_BACKENDS). Если используется стандартный обработчик аутентификации Django (ModelBackend), credentials должны быть именем пользователя и паролем, переданными как ключевые аргументы:

>>> c = Client()
>>> c.login(username="fred", password="secret")

# Now you can access a view that's only available to logged-in users.

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

login() возвращает True в случае успешного входа.

В завершение, вам нужно создать учетные записи пользователей перед использованием этого метода. Как указано выше, тестовый запуск выполняется на тестовой базе данных, которая по умолчанию не содержит пользователей. В результате учетные записи пользователей, действительные на вашем сайте, не будут работать в тестовых условиях. Вам необходимо создать пользователей как часть набора тестов — вручную (используя API модели Django) или с помощью тестовой фикстуры. Помните, что если вы хотите, чтобы у вашего тестового пользователя был пароль, вы не можете установить пароль, задав атрибут password напрямую — вы должны использовать функцию set_password() для хранения правильно хэшированного пароля. В качестве альтернативы, вы можете использовать метод create_user() для создания нового пользователя с правильно хэшированным паролем.

force_login(user, backend=None)

Если ваш сайт использует систему аутентификации Django, вы можете использовать метод force_login() для моделирования входа пользователя на сайт. Используйте этот метод вместо login(), когда тест требует входа пользователя, и подробности о том, как пользователь вошел, не важны.

В отличие от login(), этот метод пропускает шаги аутентификации и проверки: неактивные пользователи (is_active=False) могут войти, и учетные данные пользователя не требуются.

Атрибут пользователя backend будет установлен в значение аргумента backend (который должен быть строкой с путём к Python-классу) или в значение settings.AUTHENTICATION_BACKENDS[0], если значение не предоставлено. Функция authenticate(), вызываемая login(), обычно аннотирует пользователя таким образом.

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

logout()

Если ваш сайт использует систему аутентификации Django, метод logout() можно использовать для моделирования выхода пользователя с сайта.

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

Тестирование ответов

Методы get() и post() оба возвращают объект Response. Этот объект Response не идентичен объекту HttpResponse, возвращаемому представлениями Django; объект тестового ответа содержит дополнительные данные, полезные для кода тестов для проверки.

В частности, объект Response имеет следующие атрибуты:

class Response
client

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

content

Тело ответа в виде байтовой строки. Это окончательное содержимое страницы, сгенерированное представлением, или любое сообщение об ошибке.

context

Экземпляр шаблона Context , который использовался для рендеринга шаблона, создавшего содержимое ответа.

Если для рендеринга страницы использовалось несколько шаблонов, то context будет списком Context объектов в порядке их рендеринга.

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

>>> response = client.get("/foo/")
>>> response.context["name"]
'Arthur'

Не используется Django шаблоны?

Этот атрибут заполняется только при использовании бэкенда DjangoTemplates. Если вы используете другой движок шаблонов, context_data может быть подходящей альтернативой для ответов с этим атрибутом.

exc_info

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

Значения — (тип, значение, отслеживание стека вызовов), аналогично возвращаемым Python функцией sys.exc_info(). Их значения:

  • тип: Тип исключения.
  • значение: Экземпляр исключения.
  • отслеживание стека вызовов: Объект отслеживания стека вызовов, который описывает стек вызовов в момент первоначального возникновения исключения.

Если исключение не произошло, то exc_info будет None.

json(**kwargs)

Тело ответа, распарсенное как JSON. Дополнительные именованные аргументы передаются в json.loads(). Например:

>>> response = client.get("/foo/")
>>> response.json()["name"]
'Arthur'

Если заголовок Content-Type не "application/json", при попытке парсинга ответа будет возбуждено исключение ValueError.

request

Данные запроса, которые спровоцировали ответ.

wsgi_request

Экземпляр WSGIRequest , сгенерированный обработчиком тестов, который сгенерировал ответ.

status_code

HTTP-код состояния ответа в виде целого числа. Полный список определённых кодов см. в реестре кодов состояния IANA.

templates

Список экземпляров Template , используемых для рендеринга конечного содержимого в порядке их рендеринга. Для каждого шаблона в списке используйте template.name , чтобы получить имя файла шаблона, если шаблон был загружен из файла. (Имя — строка, например 'admin/index.html'.)

Не используется Django шаблоны?

Этот атрибут заполняется только при использовании бэкенда DjangoTemplates. Если вы используете другой движок шаблонов, template_name может быть подходящей альтернативой, если вам нужно только имя шаблона, используемого для рендеринга.

resolver_match

Экземпляр ResolverMatch для ответа. Например, вы можете использовать атрибут func для проверки представления, которое обработало ответ:

# my_view here is a function based view.
self.assertEqual(response.resolver_match.func, my_view)

# Class-based views need to compare the view_class, as the
# functions generated by as_view() won't be equal.
self.assertIs(response.resolver_match.func.view_class, MyView)

Если указанный URL не найден, доступ к этому атрибуту вызовет исключение Resolver404.

Как и при работе с обычным ответом, вы можете получить доступ к заголовкам через HttpResponse.headers. Например, вы можете определить тип содержимого ответа, используя response.headers['Content-Type'].

Исключения

Если вы направите тестовый клиент на представление, которое генерирует исключение, и Client.raise_request_exception равно True, это исключение будет видно в тестовом случае. Затем вы можете использовать стандартный блок try ... except или assertRaises() для проверки исключений.

Единственные исключения, которые не видны тестовому клиенту, это Http404, PermissionDenied, SystemExit и SuspiciousOperation. Django обрабатывает эти исключения внутренне и преобразует их в соответствующие коды HTTP-ответа. В этих случаях вы можете проверить response.status_code в своем тесте.

Если Client.raise_request_exception равно False, тестовый клиент вернёт ответ 500, как и в случае с браузером. Ответ содержит атрибут exc_info для получения информации об обработанном исключении.

Состояние

Тестовый клиент имеет состояние. Если ответ возвращает cookie, то эта cookie будет сохранена в тестовом клиенте и отправлена со всеми последующими get() и post() запросами.

Политики истечения срока действия для этих cookie не соблюдаются. Если вы хотите, чтобы cookie истекла, удалите её вручную или создайте новый экземпляр Client (что фактически удалит все cookie).

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

Client.cookies

Объект Python SimpleCookie, содержащий текущие значения всех cookie клиента. См. документацию модуля http.cookies для получения дополнительной информации.

Client.session

Объект, похожий на словарь, содержащий информацию о сессии. Подробности см. в документации по сессиям.

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

def test_something(self):
    session = self.client.session
    session["somekey"] = "test"
    session.save()

Установка языка

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

Если middleware включён, язык можно установить, создав cookie с именем LANGUAGE_COOKIE_NAME и значением кода языка:

from django.conf import settings


def test_language_using_cookie(self):
    self.client.cookies.load({settings.LANGUAGE_COOKIE_NAME: "fr"})
    response = self.client.get("/")
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

или включив заголовок Accept-Language HTTP в запросе:

def test_language_using_header(self):
    response = self.client.get("/", headers={"accept-language": "fr"})
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

Примечание

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

def tearDown(self):
    translation.activate(settings.LANGUAGE_CODE)

Дополнительные сведения см. в Как Django определяет предпочтение языка.

Если middleware не включён, активный язык можно установить с помощью translation.override():

from django.utils import translation


def test_language_using_override(self):
    with translation.override("fr"):
        response = self.client.get("/")
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

Дополнительные сведения см. в Явное установление активного языка.

Пример

Следующий пример демонстрирует unit-тест с использованием тестового клиента:

import unittest
from django.test import Client


class SimpleTest(unittest.TestCase):
    def setUp(self):
        # Every test needs a client.
        self.client = Client()

    def test_details(self):
        # Issue a GET request.
        response = self.client.get("/customer/details/")

        # Check that the response is 200 OK.
        self.assertEqual(response.status_code, 200)

        # Check that the rendered context contains 5 customers.
        self.assertEqual(len(response.context["customers"]), 5)

См. также

django.test.RequestFactory

Предоставленные классы тестовых случаев

Обычные классы Python-тестов наследуют базовый класс unittest.TestCase. Django предоставляет несколько расширений этого базового класса:

Иерархия классов Django для юнит-тестирования (подклассы TestCase)

Иерархия классов Django для модульного тестирования

Вы можете преобразовать обычный unittest.TestCase в любой из подклассов: измените базовый класс вашего теста с unittest.TestCase на подкласс. Все стандартные функции тестирования Python будут доступны, и они будут дополнены полезными дополнениями, как описано в каждом разделе ниже.

SimpleTestCase

class SimpleTestCase

Подкласс unittest.TestCase, который добавляет эту функциональность:

  • Некоторые полезные утверждения, такие как:
    • Проверка того, что вызываемый метод raises a certain exception.
    • Проверка того, что вызываемый метод triggers a certain warning.
    • Тестирование поля формы rendering and error treatment.
    • Тестирование HTML responses for the presence/lack of a given fragment.
    • Проверка того, что шаблон has/hasn't been used to generate a given response content.
    • Проверка того, что два URLs равны.
    • Проверка, что приложение выполняет перенаправление HTTP redirect.
    • Надежное тестирование двух HTML fragments на равенство/неравенство или containment.
    • Надежное тестирование двух XML fragments на равенство/неравенство.
    • Надежное тестирование двух JSON fragments на равенство.
  • Возможность запускать тесты с изменёнными настройками.
  • Использование client Client.

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

SimpleTestCase.databases

SimpleTestCase по умолчанию запрещает запросы к базе данных. Это помогает избежать выполнения запросов на запись, которые повлияют на другие тесты, так как каждый SimpleTestCase тест не выполняется в транзакции. Если это не вызывает беспокойства, можно отключить это поведение, установив атрибут класса databases в значение '__all__' в вашем классе тестов.

Предупреждение

SimpleTestCase и его подклассы (например, TestCase, …) полагаются на setUpClass() и tearDownClass() для выполнения некоторых инициализаций на уровне класса (например, переопределения настроек). Если вам нужно переопределить эти методы, не забудьте вызвать реализацию super:

class MyTestCase(TestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        ...

    @classmethod
    def tearDownClass(cls):
        ...
        super().tearDownClass()

Учитывайте поведение Python, если во время setUpClass() возникает исключение. В этом случае тесты в классе и tearDownClass() не выполняются. В случае с django.test.TestCase это приведёт к утечке транзакции, созданной в super(), что может привести к различным проблемам, в том числе к ошибке сегментации на некоторых платформах (задокументировано на macOS). Если вы намеренно хотите вызвать исключение, такое как unittest.SkipTest в setUpClass(), убедитесь, что вы делаете это до вызова super() для предотвращения этой проблемы.

TransactionTestCase

class TransactionTestCase

TransactionTestCase наследуется от SimpleTestCase, чтобы добавить некоторые функции, специфичные для базы данных:

  • Сброс базы данных до известного состояния в начале каждого теста для облегчения тестирования и использования ORM.
  • Фикстуры базы данных fixtures.
  • Пропуск тестов в зависимости от возможностей СУБД пропуск тестов.
  • Дополнительные специализированные методы assert*.

Класс TestCase Django — это более распространённый подкласс TransactionTestCase, который использует возможности транзакций базы данных, чтобы ускорить процесс сброса базы данных до известного состояния в начале каждого теста. Однако следствием этого является то, что некоторые поведения базы данных не могут быть протестированы внутри класса Django TestCase. Например, нельзя проверить, что блок кода выполняется в транзакции, как это требуется при использовании select_for_update(). В таких случаях следует использовать TransactionTestCase.

TransactionTestCase и TestCase идентичны, за исключением способа сброса базы данных до известного состояния и возможности проверки эффектов коммита и отката:

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

Предупреждение

TestCase, работающий с базой данных, не поддерживающей откат (например, MySQL с хранилищем MyISAM), и все экземпляры TransactionTestCase, будут отменены в конце теста удалением всех данных из тестовой базы данных.

Приложения не увидят перезагрузку своих данных; если вам нужна эта функциональность (например, сторонним приложениям следует её включить), вы можете установить serialized_rollback = True внутри блока TestCase.

TestCase

class TestCase

Этот класс чаще всего используется для написания тестов в Django. Он наследуется от TransactionTestCase (и, следовательно, от SimpleTestCase). Если ваше приложение Django не использует базу данных, используйте SimpleTestCase.

Класс:

  • Оборачивает тесты в два вложенных блока atomic(): один для всего класса и один для каждого теста. Следовательно, если вы хотите протестировать определённое поведение транзакции базы данных, используйте TransactionTestCase.
  • Проверяет отложенные ограничения базы данных в конце каждого теста.

Он также предоставляет дополнительный метод:

classmethod TestCase.setUpTestData()

Указанный выше блок уровня класса atomic позволяет создавать начальные данные на уровне класса один раз для всего TestCase. Этот метод позволяет ускорить тесты по сравнению с использованием setUp().

Например:

from django.test import TestCase


class MyTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Set up data for the whole TestCase
        cls.foo = Foo.objects.create(bar="Test")
        ...

    def test1(self):
        # Some test using self.foo
        ...

    def test2(self):
        # Some other test using self.foo
        ...

Обратите внимание, что если тесты запускаются на базе данных без поддержки транзакций (например, MySQL с движком MyISAM), setUpTestData() будет вызываться перед каждым тестом, что снижает преимущества скорости.

Объекты, присвоенные атрибутам класса в setUpTestData(), должны поддерживать создание глубоких копий с помощью copy.deepcopy() для изоляции их от изменений, выполняемых методами каждого теста.

END_OF_DOCUMENT_MARKER
classmethod TestCase.captureOnCommitCallbacks(using=DEFAULT_DB_ALIAS, execute=False)

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

using — это псевдоним подключения к базе данных для захвата коллбеков.

Если execute равно True, все коллбеки будут вызваны при выходе из менеджера контекста, если не произошла ошибка. Это имитирует коммит после обернутого блока кода.

Например:

from django.core import mail
from django.test import TestCase


class ContactTests(TestCase):
    def test_post(self):
        with self.captureOnCommitCallbacks(execute=True) as callbacks:
            response = self.client.post(
                "/contact/",
                {"message": "I like your site"},
            )

        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(callbacks), 1)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, "Contact Form")
        self.assertEqual(mail.outbox[0].body, "I like your site")

LiveServerTestCase

class LiveServerTestCase

LiveServerTestCase в основном делает то же самое, что и TransactionTestCase, но с одной дополнительной функцией: он запускает живой сервер Django на заднем плане при настройке и закрывает его при завершении. Это позволяет использовать автоматические тестовые клиенты, отличные от Django dummy client, такие как, например, клиент Selenium, для выполнения серии функциональных тестов в браузере и имитации действий реального пользователя.

Живой сервер слушает на localhost и привязывается к порту 0, который использует свободный порт, назначенный операционной системой. К URL-адресу сервера можно получить доступ с помощью self.live_server_url во время тестов.

Чтобы продемонстрировать, как использовать LiveServerTestCase, давайте напишем Selenium-тест. Прежде всего, вам необходимо установить пакет selenium в ваш путь Python:

$ python -m pip install selenium
...\> py -m pip install selenium

Затем добавьте тест, основанный на LiveServerTestCase, в модуль тестов вашего приложения (например: myapp/tests.py). В этом примере мы будем предполагать, что вы используете приложение staticfiles и хотите, чтобы статические файлы обрабатывались во время выполнения ваших тестов аналогично тому, как это происходит во время разработки с DEBUG=True, то есть без необходимости их сбора с помощью collectstatic. Мы будем использовать подкласс StaticLiveServerTestCase, который предоставляет эту функциональность. Замените его на django.test.LiveServerTestCase если вам это не нужно.

Код для этого теста может выглядеть следующим образом:

from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver


class MySeleniumTests(StaticLiveServerTestCase):
    fixtures = ["user-data.json"]

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.selenium = WebDriver()
        cls.selenium.implicitly_wait(10)

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()

    def test_login(self):
        self.selenium.get(f"{self.live_server_url}/login/")
        username_input = self.selenium.find_element(By.NAME, "username")
        username_input.send_keys("myuser")
        password_input = self.selenium.find_element(By.NAME, "password")
        password_input.send_keys("secret")
        self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()

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

$ ./manage.py test myapp.tests.MySeleniumTests.test_login
...\> manage.py test myapp.tests.MySeleniumTests.test_login

В этом примере автоматически откроется Firefox, затем откроется страница входа, будут введены учетные данные и нажата кнопка «Войти». Selenium предлагает других драйверов на случай, если у вас не установлен Firefox или вы хотите использовать другой браузер. Приведенный выше пример — лишь малая часть того, что может сделать Selenium-клиент; обратитесь к полной справке для получения дополнительной информации.

Примечание

При использовании памяти SQLite для выполнения тестов одно и то же подключение к базе данных будет использоваться двумя потоками параллельно: потоком, в котором выполняется живой сервер, и потоком, в котором выполняется тестовый случай. Важно предотвратить одновременные запросы к базе данных через это общее подключение двумя потоками, поскольку это иногда может случайным образом привести к сбоям тестов. Таким образом, вам необходимо убедиться, что оба потока не обращаются к базе данных одновременно. В частности, это означает, что в некоторых случаях (например, сразу после нажатия ссылки или отправки формы) вам может потребоваться проверить, получен ли ответ Selenium и загружена ли следующая страница, прежде чем продолжать дальнейшее выполнение теста. Сделайте это, например, заставив Selenium дождаться, пока тег <body> будет найден в ответе (требуется Selenium > 2.13):

def test_login(self):
    from selenium.webdriver.support.wait import WebDriverWait

    timeout = 2
    ...
    self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()
    # Wait until the response is received
    WebDriverWait(self.selenium, timeout).until(
        lambda driver: driver.find_element(By.TAG_NAME, "body")
    )

Сложность заключается в том, что в современных веб-приложениях, которые генерируют HTML динамически после того, как сервер сгенерирует начальный документ, «загрузки страницы» в действительности не существует. Поэтому проверка наличия тега <body> в ответе может не быть уместной для всех случаев использования. Обратитесь к часто задаваемым вопросам Selenium и документации Selenium для получения дополнительной информации.

Функции тестовых случаев

Клиент по умолчанию для тестирования

SimpleTestCase.client

Каждый тестовый случай в экземпляре django.test.*TestCase имеет доступ к экземпляру Django тестового клиента. К этому клиенту можно обратиться как к self.client. Этот клиент пересоздается для каждого теста, поэтому вам не нужно беспокоиться о том, что состояние (например, куки) будет передаваться из одного теста в другой.

Это означает, что вместо создания экземпляра Client в каждом тесте:

import unittest
from django.test import Client


class SimpleTest(unittest.TestCase):
    def test_details(self):
        client = Client()
        response = client.get("/customer/details/")
        self.assertEqual(response.status_code, 200)

    def test_index(self):
        client = Client()
        response = client.get("/customer/index/")
        self.assertEqual(response.status_code, 200)

…вы можете обратиться к self.client, как показано ниже:

from django.test import TestCase


class SimpleTest(TestCase):
    def test_details(self):
        response = self.client.get("/customer/details/")
        self.assertEqual(response.status_code, 200)

    def test_index(self):
        response = self.client.get("/customer/index/")
        self.assertEqual(response.status_code, 200)

Настройка тестового клиента

SimpleTestCase.client_class

Если вы хотите использовать другой класс Client (например, подкласс с настроенным поведением), используйте атрибут класса client_class:

from django.test import Client, TestCase


class MyTestClient(Client):
    # Specialized methods for your environment
    ...


class MyTest(TestCase):
    client_class = MyTestClient

    def test_my_stuff(self):
        # Here self.client is an instance of MyTestClient...
        call_some_test_code()

Загрузка фикстур

TransactionTestCase.fixtures

Тестовый случай для веб-сайта с поддержкой базы данных не будет особо полезен, если в базе данных нет данных. Тесты более читаемы и поддерживаются, если создавать объекты с помощью ORM, например, в TestCase.setUpTestData(), однако вы также можете использовать фикстуры.

Фикстура — это набор данных, который Django знает, как импортировать в базу данных. Например, если ваш сайт имеет учетные записи пользователей, вы можете создать фикстуру фальшивых учетных записей пользователей, чтобы заполнить вашу базу данных во время тестов.

Самый простой способ создания фикстуры — использовать команду manage.py dumpdata. Это предполагает, что в вашей базе данных уже есть данные. Смотрите dumpdata documentation для получения дополнительной информации.

После создания фикстуры и размещения её в каталоге fixtures в одном из ваших INSTALLED_APPS, вы можете использовать её в своих юнит-тестах, указав атрибут класса fixtures для вашего подкласса django.test.TestCase:

from django.test import TestCase
from myapp.models import Animal


class AnimalTestCase(TestCase):
    fixtures = ["mammals.json", "birds"]

    def setUp(self):
        # Test definitions as before.
        call_setup_methods()

    def test_fluffy_animals(self):
        # A test that uses the fixtures.
        call_some_test_code()

Вот что именно произойдёт:

  • В начале каждого теста, перед запуском setUp(), Django очистит базу данных, вернув базу данных в состояние, в котором она находилась непосредственно после вызова migrate.
  • Затем все указанные фикстуры будут установлены. В данном примере Django установит все JSON-фикстуры с именем mammals, а затем — все фикстуры с именем birds. Обратитесь к разделу Фикстуры для получения более подробной информации о определении и установке фикстур.

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

По умолчанию фикстуры загружаются только в базу данных default. Если вы используете несколько баз данных и установили TransactionTestCase.databases, фикстуры будут загружены во все указанные базы данных.

Настройка URLconf

Если ваше приложение предоставляет представления, вам может потребоваться включить тесты, которые используют тестовый клиент для проверки этих представлений. Однако конечный пользователь может развернуть представления в вашем приложении на любом URL-адресе по своему выбору. Это означает, что ваши тесты не могут полагаться на то, что ваши представления будут доступны по определенному URL-адресу. Используйте декоратор @override_settings(ROOT_URLCONF=...) для настройки URLconf.

Поддержка нескольких баз данных

TransactionTestCase.databases

Django настраивает тестовую базу данных, соответствующую каждой базе данных, определённой в определении DATABASES в ваших настройках и упоминаемой хотя бы одним тестом через databases.

Однако большая часть времени, затрачиваемого на выполнение Django TestCase, тратится на вызов flush, который гарантирует наличие чистой базы данных в начале каждого запуска теста. Если у вас несколько баз данных, требуется несколько очисток (по одной для каждой базы данных), что может быть длительным процессом — особенно если ваши тесты не требуют проверки взаимодействия с несколькими базами данных.

В целях оптимизации, Django сбрасывает базу данных default только в начале каждого запуска тестов. Если ваша настройка содержит несколько баз данных, и у вас есть тест, который требует очистки каждой базы данных, вы можете использовать атрибут databases в наборе тестов, чтобы запросить дополнительную очистку баз данных.

Например:

class TestMyViews(TransactionTestCase):
    databases = {"default", "other"}

    def test_index_page_view(self):
        call_some_test_code()

Этот тест-кейс очистит тестовые базы данных default и other перед запуском test_index_page_view. Также можно использовать '__all__' для указания необходимости очистки всех тестовых баз данных.

Флаг databases также управляет тем, в какие базы данных будут загружены TransactionTestCase.fixtures. По умолчанию фикстуры загружаются только в базу данных default.

Запросы к базам данных, не включенным в databases , приведут к ошибкам утверждения для предотвращения утечки состояния между тестами.

TestCase.databases

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

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

Например:

class OtherDBTests(TestCase):
    databases = {"other"}

    def test_other_db_query(self):
        ...

Этот тест позволит выполнять запросы только к базе данных other. Так же, как и для SimpleTestCase.databases и TransactionTestCase.databases, константа '__all__' может использоваться для указания, что тест должен разрешить запросы ко всем базам данных.

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

Предупреждение

Используйте указанные ниже функции для временного изменения значений настроек в тестах. Не манипулируйте django.conf.settings напрямую, так как Django не восстановит исходные значения после таких манипуляций.

SimpleTestCase.settings()

Для целей тестирования часто бывает полезно временно изменить настройку и вернуть исходное значение после выполнения кода тестирования. Для этого Django предоставляет стандартный менеджер контекста Python (см. PEP 343), называемый settings(), который можно использовать так:

from django.test import TestCase


class LoginTestCase(TestCase):
    def test_login(self):
        # First check for the default behavior
        response = self.client.get("/sekrit/")
        self.assertRedirects(response, "/accounts/login/?next=/sekrit/")

        # Then override the LOGIN_URL setting
        with self.settings(LOGIN_URL="/other/login/"):
            response = self.client.get("/sekrit/")
            self.assertRedirects(response, "/other/login/?next=/sekrit/")

В данном примере будет переопределена настройка LOGIN_URL для кода в блоке with и после этого её значение будет сброшено к предыдущему состоянию.

SimpleTestCase.modify_settings()

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

from django.test import TestCase


class MiddlewareTestCase(TestCase):
    def test_cache_middleware(self):
        with self.modify_settings(
            MIDDLEWARE={
                "append": "django.middleware.cache.FetchFromCacheMiddleware",
                "prepend": "django.middleware.cache.UpdateCacheMiddleware",
                "remove": [
                    "django.contrib.sessions.middleware.SessionMiddleware",
                    "django.contrib.auth.middleware.AuthenticationMiddleware",
                    "django.contrib.messages.middleware.MessageMiddleware",
                ],
            }
        ):
            response = self.client.get("/")
            # ...

Для каждого действия вы можете предоставить либо список значений, либо строку. Когда значение уже существует в списке, append и prepend не имеют эффекта; то же самое касается remove при отсутствии значения.

override_settings(**kwargs)

В случае необходимости переопределения настройки для метода теста Django предоставляет декоратор override_settings() (см. PEP 318). Он используется следующим образом:

from django.test import TestCase, override_settings


class LoginTestCase(TestCase):
    @override_settings(LOGIN_URL="/other/login/")
    def test_login(self):
        response = self.client.get("/sekrit/")
        self.assertRedirects(response, "/other/login/?next=/sekrit/")

Декоратор также может быть применен к классам TestCase:

from django.test import TestCase, override_settings


@override_settings(LOGIN_URL="/other/login/")
class LoginTestCase(TestCase):
    def test_login(self):
        response = self.client.get("/sekrit/")
        self.assertRedirects(response, "/other/login/?next=/sekrit/")
modify_settings(*args, **kwargs)

Аналогично, Django предоставляет декоратор modify_settings():

from django.test import TestCase, modify_settings


class MiddlewareTestCase(TestCase):
    @modify_settings(
        MIDDLEWARE={
            "append": "django.middleware.cache.FetchFromCacheMiddleware",
            "prepend": "django.middleware.cache.UpdateCacheMiddleware",
        }
    )
    def test_cache_middleware(self):
        response = self.client.get("/")
        # ...

Декоратор также может быть применен к классам тестовых случаев:

from django.test import TestCase, modify_settings


@modify_settings(
    MIDDLEWARE={
        "append": "django.middleware.cache.FetchFromCacheMiddleware",
        "prepend": "django.middleware.cache.UpdateCacheMiddleware",
    }
)
class MiddlewareTestCase(TestCase):
    def test_cache_middleware(self):
        response = self.client.get("/")
        # ...

Примечание

При применении к классу эти декораторы модифицируют класс напрямую и возвращают его; они не создают и не возвращают изменённую копию. Поэтому, если вы попытаетесь изменить примеры выше, чтобы присвоить возвращаемое значение другому имени, кроме LoginTestCase или MiddlewareTestCase, вы можете быть удивлены, обнаружив, что исходные тестовые классы по-прежнему подвержены влиянию декоратора. Для данного класса modify_settings() всегда применяется после override_settings().

Предупреждение

Файл настроек содержит некоторые настройки, которые используются только во время инициализации внутренних компонентов Django. Если вы измените их с помощью override_settings, настройка изменится, если вы обратитесь к ней через модуль django.conf.settings, однако внутренние компоненты Django обращаются к ней по-другому. Фактически, использование override_settings() или modify_settings() с этими настройками, вероятно, не даст ожидаемого результата.

Не рекомендуется изменять настройку DATABASES. Изменение настройки CACHES возможно, но немного сложно, если вы используете внутренние компоненты, использующие кэширование, такие как django.contrib.sessions. Например, вам придётся повторно инициализировать бэкэнд сессий в тесте, использующем кэшированные сессии и переопределяющем CACHES.

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

Вы также можете смоделировать отсутствие настройки, удалив её после переопределения настроек, например, так:

@override_settings()
def test_something(self):
    del settings.LOGIN_URL
    ...

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

Сам Django использует этот сигнал для сброса различных данных:

Переопределённые настройки Сброшенные данные
USE_TZ, TIME_ZONE Часовой пояс баз данных
TEMPLATES Движки шаблонов
SERIALIZATION_MODULES Кэш сериализаторов
LOCALE_PATHS, LANGUAGE_CODE Стандартный перевод и загруженные переводы
DEFAULT_FILE_STORAGE, STATICFILES_STORAGE, STATIC_ROOT, STATIC_URL, STORAGES Настройка хранилищ

Изоляция приложений

utils.isolate_apps(*app_labels, attr_name=None, kwarg_name=None)

Регистрирует модели, определённые в обернутом контексте, в собственной изолированной регистре apps. Эта функциональность полезна при создании классов моделей для тестов, так как классы будут надёжно удалены после этого, и не возникнет проблем с совпадением имён.

Метки приложений, которые должны содержаться в изолированном регистре, должны передаваться как отдельные аргументы. Вы можете использовать isolate_apps() в качестве декоратора или менеджера контекста. Например:

from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps


class MyModelTests(SimpleTestCase):
    @isolate_apps("app_label")
    def test_model_definition(self):
        class TestModel(models.Model):
            pass

        ...

… или:

with isolate_apps("app_label"):

    class TestModel(models.Model):
        pass

    ...

Форма декоратора также может быть применена к классам.

Можно указать два необязательных ключевых аргумента:

  • attr_name: атрибут, которому присваивается изолированный регистр, если используется в качестве декоратора класса.
  • kwarg_name: ключевой аргумент, передающий изолированный регистр, если используется в качестве декоратора функции.

Временный экземпляр Apps , используемый для изоляции регистрации моделей, может быть получен как атрибут при использовании в качестве декоратора класса, используя параметр attr_name:

@isolate_apps("app_label", attr_name="apps")
class TestModelDefinition(SimpleTestCase):
    def test_model_definition(self):
        class TestModel(models.Model):
            pass

        self.assertIs(self.apps.get_model("app_label", "TestModel"), TestModel)

… или альтернативно в качестве аргумента в методе теста при использовании в качестве декоратора метода, используя параметр kwarg_name:

class TestModelDefinition(SimpleTestCase):
    @isolate_apps("app_label", kwarg_name="apps")
    def test_model_definition(self, apps):
        class TestModel(models.Model):
            pass

        self.assertIs(apps.get_model("app_label", "TestModel"), TestModel)

Очистка тестового ящика вывода

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

Для получения дополнительной информации об услугах электронной почты во время тестов см. Услуги электронной почты ниже.

Утверждения

Так как стандартный класс Python unittest.TestCase реализует методы утверждения, такие как assertTrue() и assertEqual(), пользовательский класс Django TestCase предоставляет ряд пользовательских методов утверждений, которые полезны для тестирования веб-приложений:

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

SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs)
SimpleTestCase.assertRaisesMessage(expected_exception, expected_message)

Утверждает, что выполнение callable вызывает expected_exception и что expected_message содержится в сообщении об исключении. Любой другой результат считается ошибкой. Это упрощенная версия unittest.TestCase.assertRaisesRegex() с отличием, что expected_message не обрабатывается как регулярное выражение.

Если указаны только параметры expected_exception и expected_message, возвращается менеджер контекста, чтобы код, который тестируется, можно было записать непосредственно, а не в виде функции:

with self.assertRaisesMessage(ValueError, "invalid literal for int()"):
    int("a")
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message, callable, *args, **kwargs)
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message)

Аналогично SimpleTestCase.assertRaisesMessage(), но для assertWarnsRegex() вместо assertRaisesRegex().

SimpleTestCase.assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value='')

Утверждает, что поле формы ведет себя корректно при различных входных данных.

Параметры:
  • fieldclass – класс поля, подлежащего тестированию.
  • valid – словарь, сопоставляющий допустимые входные данные с ожидаемыми очищенными значениями.
  • invalid – словарь, сопоставляющий недопустимые входные данные с одним или несколькими сообщениями об ошибках, которые были подняты.
  • field_args – аргументы, переданные для создания поля.
  • field_kwargs – ключевые аргументы, переданные для создания поля.
  • empty_value – ожидаемый результат очистки для входных данных в empty_values.

Например, следующий код проверяет, что поле EmailField принимает a@a.com как допустимый адрес электронной почты, но отклоняет aaa с разумным сообщением об ошибке:

self.assertFieldOutput(
    EmailField, {"a@a.com": "a@a.com"}, {"aaa": ["Enter a valid email address."]}
)
SimpleTestCase.assertFormError(form, field, errors, msg_prefix='')

Утверждает, что поле на форме вызывает предоставленный список ошибок.

form — это экземпляр Form. Форма должна быть связанной, но не обязательно проверенной (assertFormError() автоматически вызовет full_clean() для формы).

field — это имя поля на форме, которое нужно проверить. Чтобы проверить не связанные с полями ошибки формы, используйте field=None.

errors — это список всех ожидаемых сообщений об ошибках поля. Вы также можете передать одну строку ошибки, если ожидается только одна ошибка, что означает, что errors='error message' эквивалентно errors=['error message'].

Изменено в Django 4.1:

В более ранних версиях использование пустого списка ошибок с assertFormError() всегда считалось успешным, независимо от того, есть ли у поля ошибки или нет. Начиная с Django 4.1, использование errors=[] считается успешным только в том случае, если у поля фактически нет ошибок.

Django 4.1 также изменил поведение assertFormError(), когда у поля несколько ошибок. В более ранних версиях, если у поля было несколько ошибок, и вы проверяли только некоторые из них, тест проходил. Начиная с Django 4.1, список ошибок должен точно совпадать с фактическими ошибками поля.

Устарело начиная с версии 4.1: Поддержка передачи объекта ответа и имени формы в assertFormError() устарела и будет удалена в Django 5.0. Используйте экземпляр формы напрямую вместо этого.

SimpleTestCase.assertFormSetError(formset, form_index, field, errors, msg_prefix='')

Утверждает, что formset вызывает предоставленный список ошибок при рендеринге.

formset — это экземпляр FormSet. Formset должен быть связан, но не обязательно валидирован (assertFormSetError() автоматически вызовет full_clean() для formset).

form_index — это номер формы в FormSet (начиная с 0). Используйте form_index=None для проверки ошибок formset, не связанных с формами, т.е. ошибок, которые вы получаете при вызове formset.non_form_errors(). В этом случае вы также должны использовать field=None.

field и errors имеют то же значение, что и параметры в assertFormError().

Устарело начиная с версии 4.1: Поддержка передачи объекта ответа и имени formset в assertFormSetError() устарела и будет удалена в Django 5.0. Используйте экземпляр formset напрямую вместо этого.

Устарело начиная с версии 4.2: Метод утверждения assertFormsetError() устарел. Используйте assertFormSetError() вместо него.

SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False)

Утверждает, что response произвел указанный status_code и что text появляется в его content. Если count предоставлено, text должно появляться ровно count раз в ответе.

Установите html на True, чтобы обработать text как HTML. Сравнение с содержимым ответа будет основано на семантике HTML, а не на точном равенстве символов. Пробелы игнорируются в большинстве случаев, порядок атрибутов не имеет значения. Подробнее см. assertHTMLEqual().

SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False)

Утверждает, что response произвел указанный status_code и что text не появляется в его content.

Установите html на True, чтобы обработать text как HTML. Сравнение с содержимым ответа будет основано на семантике HTML, а не на точном равенстве символов. Пробелы игнорируются в большинстве случаев, порядок атрибутов не имеет значения. Подробнее см. assertHTMLEqual().

SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix='', count=None)

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

response должен быть экземпляром ответа, возвращённым методом test client.

template_name должна быть строкой, например, 'admin/index.html'.

Аргумент count — целое число, указывающее количество раз, которое шаблон должен быть рендерен. По умолчанию None, что означает, что шаблон должен быть рендерен один или несколько раз.

Вы можете использовать это как менеджер контекста, так:

with self.assertTemplateUsed("index.html"):
    render_to_string("index.html")
with self.assertTemplateUsed(template_name="index.html"):
    render_to_string("index.html")
SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix='')

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

Вы можете использовать это как менеджер контекста так же, как и assertTemplateUsed().

SimpleTestCase.assertURLEqual(url1, url2, msg_prefix='')

Утверждает, что два URL-адреса одинаковы, игнорируя порядок параметров строки запроса, кроме параметров с одинаковым именем. Например, /path/?x=1&y=2 эквивалентно /path/?y=2&x=1, но /path/?a=1&a=2 не эквивалентно /path/?a=2&a=1.

END_OF_DOCUMENT_MARKER
SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='', fetch_redirect_response=True)

Утверждает, что response вернул код перенаправления status_code, перенаправив на expected_url (включая любые GET данные), и что конечная страница была получена с target_status_code.

Если для вашего запроса использовался аргумент follow, то expected_url и target_status_code будут содержать URL и код состояния для конечной точки цепочки перенаправлений.

Если fetch_redirect_response имеет значение False, конечная страница не будет загружена. Поскольку тестовый клиент не может получать внешние URL-адреса, это особенно полезно, если expected_url не входит в ваш проект Django.

Схема обрабатывается корректно при сравнении двух URL-адресов. Если в месте перенаправления не указана схема, используется схема исходного запроса. Если указана, схема в expected_url используется для сравнения.

SimpleTestCase.assertHTMLEqual(html1, html2, msg=None)

Утверждает, что строки html1 и html2 равны. Сравнение основано на семантике HTML. Сравнение учитывает следующие моменты:

  • Пробелы до и после тегов HTML игнорируются.
  • Все типы пробелов считаются эквивалентными.
  • Все открытые теги закрываются неявно, например, когда закрывается окружающий тег или заканчивается HTML-документ.
  • Пустые теги эквивалентны их самозакрывающимся версиям.
  • Порядок атрибутов HTML-элемента не важен.
  • Булевы атрибуты (например, checked) без аргумента равны атрибутам с одинаковым именем и значением (см. примеры).
  • Текст, числовые ссылки и ссылки на сущности, которые ссылаются на один и тот же символ, эквивалентны.

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

self.assertHTMLEqual(
    "<p>Hello <b>&#x27;world&#x27;!</p>",
    """<p>
        Hello   <b>&#39;world&#39;! </b>
    </p>""",
)
self.assertHTMLEqual(
    '<input type="checkbox" checked="checked" id="id_accept_terms" />',
    '<input id="id_accept_terms" type="checkbox" checked>',
)

html1 и html2 должны содержать HTML. Будет возбуждено исключение AssertionError, если один из них не может быть проанализирован.

Вывод при ошибке можно настроить с помощью аргумента msg.

SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None)

Утверждает, что строки html1 и html2 не равны. Сравнение основано на семантике HTML. См. assertHTMLEqual() для получения подробной информации.

html1 и html2 должны содержать HTML. Будет возбуждено исключение AssertionError, если один из них не может быть проанализирован.

Вывод при ошибке можно настроить с помощью аргумента msg.

SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None)

Утверждает, что строки xml1 и xml2 равны. Сравнение основано на семантике XML. Аналогично assertHTMLEqual(), сравнение выполняется на проанализированном содержимом, поэтому учитываются только семантические различия, а не синтаксические. Если в любой параметр передается некорректный XML, всегда возбуждается исключение AssertionError, даже если обе строки идентичны.

Декларация XML, тип документа, инструкции обработки и комментарии игнорируются. Сравниваются только корневой элемент и его потомки.

Вывод при ошибке можно настроить с помощью аргумента msg.

SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None)

Утверждает, что строки xml1 и xml2 не равны. Сравнение основано на семантике XML. См. assertXMLEqual() для получения подробной информации.

Вывод при ошибке можно настроить с помощью аргумента msg.

SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix='')

Утверждает, что HTML-фрагмент needle содержится в haystack один раз.

Если указан целочисленный аргумент count, то дополнительно будет строго проверяться количество needle вхождений.

Пробелы в большинстве случаев игнорируются, а порядок атрибутов не имеет значения. См. assertHTMLEqual() для получения более подробной информации.

SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None)

Утверждает, что JSON-фрагменты raw и expected_data равны. Стандартные правила JSON для игнорирования пробелов применяются, так как тяжелая обработка делегирована библиотеке json.

Вывод при ошибке можно настроить с помощью аргумента msg.

SimpleTestCase.assertJSONNotEqual(raw, expected_data, msg=None)

Утверждает, что JSON-фрагменты raw и expected_data не равны. См. assertJSONEqual() для получения дополнительной информации.

Вывод при ошибке можно настроить с помощью аргумента msg.

TransactionTestCase.assertQuerySetEqual(qs, values, transform=None, ordered=True, msg=None)

Утверждает, что набор запросов qs соответствует определённому итерируемому набору значений values.

Если transform указано, values сравнивается со списком, полученным путём применения transform к каждому элементу qs.

По умолчанию сравнение также зависит от порядка. Если qs не предоставляет явного порядка, можно установить параметр ordered в False, что превращает сравнение в сравнение по содержанию. Если порядок неопределён (если предоставленный qs не упорядочен и сравнение проводится с более чем одним упорядоченным значением), возбуждается исключение ValueError.

Вывод при ошибке можно настроить с помощью аргумента msg.

Устарело начиная с версии 4.2: Метод утверждения assertQuerysetEqual() устарел. Используйте assertQuerySetEqual() вместо него.

TransactionTestCase.assertNumQueries(num, func, *args, **kwargs)

Утверждает, что при вызове func с *args и **kwargs выполняется num баз данных запросов.

Если ключ "using" присутствует в kwargs, он используется в качестве псевдонима базы данных, для которого следует проверять количество запросов:

self.assertNumQueries(7, using="non_default_db")

Если необходимо вызвать функцию с параметром using, можно сделать это, обернув вызов в lambda, чтобы добавить дополнительный параметр:

self.assertNumQueries(7, lambda: my_function(using=7))

Также можно использовать это как менеджер контекста:

with self.assertNumQueries(2):
    Person.objects.create(name="Aaron")
    Person.objects.create(name="Daniel")

Тегирование тестов

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

from django.test import tag


class SampleTestCase(TestCase):
    @tag("fast")
    def test_fast(self):
        ...

    @tag("slow")
    def test_slow(self):
        ...

    @tag("slow", "core")
    def test_slow_but_core(self):
        ...

Также можно тегировать тест-кейс:

@tag("slow", "core")
class SampleTestCase(TestCase):
    ...

Подклассы наследуют теги от суперклассов, а методы наследуют теги от своих классов. При:

@tag("foo")
class SampleTestCaseChild(SampleTestCase):
    @tag("bar")
    def test(self):
        ...

SampleTestCaseChild.test будут помечены тегами 'slow', 'core', 'bar', и 'foo'.

Затем можно выбрать, какие тесты запускать. Например, чтобы запустить только быстрые тесты:

$ ./manage.py test --tag=fast
...\> manage.py test --tag=fast

Или чтобы запустить быстрые тесты и основной (хотя он медленный):

$ ./manage.py test --tag=fast --tag=core
...\> manage.py test --tag=fast --tag=core

Также можно исключить тесты по тегу. Чтобы запустить тесты core, если они не медленные:

$ ./manage.py test --tag=core --exclude-tag=slow
...\> manage.py test --tag=core --exclude-tag=slow

test --exclude-tag имеет приоритет перед test --tag, поэтому если тест имеет два тега, и вы выбираете один из них, а другой исключаете, тест не будет запущен.

Тестирование асинхронного кода

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

Однако, если вы хотите написать полностью асинхронные тесты для проекта Django, вам нужно учитывать несколько моментов.

Во-первых, ваши тесты должны быть async def методами в классе тестов (чтобы предоставить им асинхронный контекст). Django автоматически обнаружит любые async def тесты и обернёт их, чтобы они выполнялись в собственном цикле событий.

Если вы тестируете функцию из асинхронной функции, вы также должны использовать асинхронный тестовый клиент. Он доступен как django.test.AsyncClient, или как self.async_client в любом тесте.

class AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, **defaults)

AsyncClient имеет те же методы и сигнатуры, что и синхронный (обычный) тестовый клиент, с двумя исключениями:

  • При инициализации произвольные ключевые аргументы в defaults добавляются непосредственно в область ASGI.
  • Параметр follow не поддерживается.
  • Заголовки, переданные как ключевые аргументы extra, не должны иметь префикс HTTP_, необходимый для синхронного клиента (см. Client.get()). Например, вот как установить HTTP-заголовок Accept:

    >>> c = AsyncClient()
    >>> c.get("/customers/details/", {"name": "fred", "age": 7}, ACCEPT="application/json")
    
Изменено в Django 4.2:

Добавлен параметр headers.

Используя AsyncClient любой метод, который делает запрос, должен быть ожидаемым:

async def test_my_thing(self):
    response = await self.async_client.get("/some-url/")
    self.assertEqual(response.status_code, 200)

Асинхронный клиент также может вызывать синхронные представления; он выполняется через асинхронный путь запроса Django, который поддерживает оба. Любое представление, вызываемое через AsyncClient, получит объект ASGIRequest для своего request, а не WSGIRequest, создаваемого обычным клиентом.

Предупреждение

Если вы используете декораторы тестов, они должны быть совместимы с асинхронностью, чтобы гарантировать их правильную работу. Встроенные декораторы Django будут работать корректно, но сторонние декораторы могут выглядеть так, как будто они не выполняются (они «обернут» неправильную часть потока выполнения, а не ваш тест).

Если вам необходимо использовать эти декораторы, то вы должны украсить методы ваших тестов с помощью async_to_sync() внутри них:

from asgiref.sync import async_to_sync
from django.test import TestCase


class MyTests(TestCase):
    @mock.patch(...)
    @async_to_sync
    async def test_my_thing(self):
        ...

Службы электронной почты

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

Тестовый запуск достигает этого, прозрачно заменяя обычный бэкенд электронной почты бэкендом для тестирования. (Не беспокойтесь — это не влияет на другие отправители электронных писем за пределами Django, такие как почтовый сервер вашей машины, если вы его используете.)

django.core.mail.outbox

Во время выполнения теста каждое исходящее электронное письмо сохраняется в django.core.mail.outbox. Это список всех EmailMessage экземпляров, которые были отправлены. Атрибут outbox — это специальный атрибут, который создается только при использовании бэкенда тестирования электронной почты. Он обычно не существует как часть модуля django.core.mail, и вы не можете импортировать его напрямую. Приведенный ниже код показывает, как правильно получить доступ к этому атрибуту.

Вот пример теста, который проверяет django.core.mail.outbox по длине и содержимому:

from django.core import mail
from django.test import TestCase


class EmailTest(TestCase):
    def test_send_email(self):
        # Send message.
        mail.send_mail(
            "Subject here",
            "Here is the message.",
            "from@example.com",
            ["to@example.com"],
            fail_silently=False,
        )

        # Test that one message has been sent.
        self.assertEqual(len(mail.outbox), 1)

        # Verify that the subject of the first message is correct.
        self.assertEqual(mail.outbox[0].subject, "Subject here")

Как отмечалось ранее, тестовый ящик выходящей почты очищается в начале каждого теста в Django *TestCase. Чтобы очистить ящик выходящей почты вручную, присвойте пустой список mail.outbox:

from django.core import mail

# Empty the test outbox
mail.outbox = []

Команды управления

Команды управления можно тестировать с помощью функции call_command(). Вывод можно перенаправить в экземпляр StringIO:

from io import StringIO
from django.core.management import call_command
from django.test import TestCase


class ClosepollTest(TestCase):
    def test_command_output(self):
        out = StringIO()
        call_command("closepoll", stdout=out)
        self.assertIn("Expected output", out.getvalue())

Пропуск тестов

Библиотека unittest предоставляет декораторы @skipIf и @skipUnless, которые позволяют пропустить тесты, если заранее известно, что эти тесты могут завершиться неудачей в определенных условиях.

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

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

Декораторы используют строковый идентификатор для описания свойств базы данных. Эта строка соответствует атрибутам класса функций свойств подключения к базе данных. Полный список свойств базы данных, которые могут быть использованы в качестве основы для пропуска тестов, см. в django.db.backends.base.features.BaseDatabaseFeatures class.

skipIfDBFeature(*feature_name_strings)

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

Например, следующий тест не будет выполнен, если база данных поддерживает транзакции (например, он не будет выполнен под PostgreSQL, но будет под MySQL с таблицами MyISAM):

class MyTests(TestCase):
    @skipIfDBFeature("supports_transactions")
    def test_transaction_behavior(self):
        # ... conditional test code
        pass
skipUnlessDBFeature(*feature_name_strings)

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

Например, следующий тест будет выполнен только в том случае, если база данных поддерживает транзакции (например, он будет выполнен под PostgreSQL, но не под MySQL с таблицами MyISAM):

class MyTests(TestCase):
    @skipUnlessDBFeature("supports_transactions")
    def test_transaction_behavior(self):
        # ... conditional test code
        pass

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/4.2/topics/testing/tools/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API