Spec-Zone.ru › Django 6.0

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

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 тестовый клиент использует конфигурацию URL, на которую указывает параметр ROOT_URLCONF.
  • Хотя приведенный выше пример работает в интерактивном интерпретаторе Python, некоторые функции тестового клиента, в частности связанные с шаблонами, доступны только во время выполнения тестов.

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

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

    Если по какой-либо причине вы хотите, чтобы тестовый клиент выполнял проверки 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, query_params=None, **defaults) [исходный код]

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

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

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

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

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

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

Примечание

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

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

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

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

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

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

get(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra) [исходный код]

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

Пары «ключ — значение» в словаре query_params используются для задания строк запроса. Например:

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

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

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

Эти параметры также можно передать в параметр data. Однако предпочтительно использовать query_params, так как он подходит для любого HTTP-метода.

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

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

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

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

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

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

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

Если URL содержит как URL-кодированные данные GET, так и аргумент query_params или 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-запрос.

post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, query_params=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-запросе без изменений, а в HTTP-заголовке Content-Type будет использовано значение content_type.

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

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

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

Отправка файлов — особый случай. Чтобы отправить файл методом POST, достаточно указать имя поля файла в качестве ключа, а дескриптор нужного для загрузки файла — в качестве значения. Например, если форма содержит поля 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, query_params и extra действуют так же, как и для Client.get().

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

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

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

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

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

head(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra) [исходный код]

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

options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra) [исходный код]

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

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

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

put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra) [исходный код]

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

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

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

patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra) [исходный код]

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

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

delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra) [исходный код]

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

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

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

trace(path, follow=False, secure=False, *, headers=None, query_params=None, **extra) [исходный код]

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

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

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

login(**credentials)
alogin(**credentials)

Асинхронная версия: alogin()

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

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

Формат аргумента 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)
aforce_login(user, backend=None)

Асинхронная версия: aforce_login()

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

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

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

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

logout()
alogout()

Асинхронная версия: alogout()

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

После вызова этого метода все файлы cookie и данные сеанса тестового клиента будут сброшены к значениям по умолчанию. Последующие запросы будут выглядеть так, будто они поступают от 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

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

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

  • type: тип исключения.
  • value: экземпляр исключения.
  • traceback: объект трассировки стека, содержащий стек вызовов на момент возникновения исключения.

Если исключение не возникло, 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, она сохраняется в тестовом клиенте и отправляется со всеми последующими запросами 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()
Client.asession()

Этот метод похож на атрибут session, но работает в асинхронных контекстах.

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

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

Если промежуточное ПО включено, язык можно задать, создав 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.")

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

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 определяет предпочтительный язык.

Если промежуточное ПО не включено, активный язык можно задать с помощью 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.")

Подробнее см. в разделе Явная установка активного языка.

Пример

Ниже приведен модульный тест с использованием тестового клиента:

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 предоставляет несколько расширений этого базового класса:

Hierarchy of Django unit testing classes (TestCase subclasses)

Иерархия классов модульного тестирования 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*.

Класс Django TestCase — это более широко используемый подкласс 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(), чтобы изолировать их от изменений, вносимых каждым методом теста.

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, например клиент Selenium, чтобы выполнять серию функциональных тестов в браузере и имитировать действия реального пользователя.

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

Чтобы показать, как использовать LiveServerTestCase, напишем тест Selenium. Для начала нужно установить пакет selenium:

$ python -m pip install "selenium >= 4.23.0"
...\> py -m pip install "selenium >= 4.23.0"

Затем добавьте в модуль тестов вашего приложения тест на основе 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 получил ответ и загрузил следующую страницу. Например, дождитесь, пока в ответе не будет найден HTML-тег <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. Этот клиент создаётся заново для каждого теста, поэтому вам не нужно беспокоиться о переносе состояния (например, файлов cookie) из одного теста в другой.

Это означает, что вместо создания экземпляра 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()

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

  • Во время setUpClass() будут установлены все указанные фикстуры. В этом примере Django установит все фикстуры JSON с именем mammals, а затем все фикстуры с именем birds. Подробнее об определении и установке фикстур см. в разделе Фикстуры.

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

  • В конце каждого теста Django очистит базу данных, вернув её в состояние, в котором она находилась сразу после вызова migrate.
  • Перед запуском setUp() каждого последующего теста фикстуры будут загружены заново.

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

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

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

Для TransactionTestCase фикстуры стали доступны во время setUpClass().

Настройка URLconf

Если ваше приложение предоставляет представления, вы можете включить тесты, в которых тестовый клиент используется для проверки этих представлений. Однако конечный пользователь может развернуть представления вашего приложения по любым URL-адресам на своё усмотрение. Это значит, что тесты не могут полагаться на то, что ваши представления будут доступны по определённому URL-адресу. Для настройки URLconf примените к тестовому классу или методу декоратор @override_settings(ROOT_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

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

Используйте атрибут класса 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

Движки шаблонов

FORM_RENDERER

Рендерер по умолчанию

SERIALIZATION_MODULES

Кэш сериализаторов

LOCALE_PATHS, LANGUAGE_CODE

Перевод по умолчанию и загруженные переводы

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 — это имя поля формы, которое нужно проверить. Чтобы проверить non-field errors формы, используйте field=None.

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

SimpleTestCase.assertFormSetError(formset, form_index, field, errors, msg_prefix='') [исходный код]

Проверяет, что при отображении formset возникают указанные ошибки.

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

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

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

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.

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.assertNotInHTML(needle, haystack, msg_prefix='') [исходный код]

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

В большинстве случаев пробелы игнорируются, а порядок атрибутов не имеет значения. Подробнее см. в описании 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, чтобы сравнение выполнялось как сравнение collections.Counter. Если порядок не определён (заданный qs не упорядочен, а сравнивается более чем с одним упорядоченным значением), вызывается ValueError.

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

TransactionTestCase.assertNumQueries(num, func, *args, **kwargs) [исходный код]

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

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

self.assertNumQueries(7, my_function, 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

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

$ ./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, query_params=None, **defaults) [исходный код]

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

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

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

При использовании 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 — специальный атрибут, который создаётся только при использовании серверной части электронной почты locmem. Обычно он не является частью модуля 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")

Как отмечалось ранее, тестовый почтовый ящик очищается в начале каждого теста в *TestCase Django. Чтобы очистить почтовый ящик вручную, присвойте 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", poll_ids=[1], stdout=out)
        self.assertIn('Successfully closed poll "1"', out.getvalue())

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

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

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

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

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

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/6.0/topics/testing/tools/

Spec-Zone.ru

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