Инструменты тестирования
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, вы можете создать экземпляр клиента для тестирования, который принудительно выполняет проверки 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 в кодированном формате, вы можете использовать это кодирование вместо аргумента 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)
-
alogin(**credentials) -
Асинхронная версия:
alogin()Если ваш сайт использует систему аутентификации 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()для создания нового пользователя с правильно захешированным паролем.Изменено в Django 5.0:Добавлен метод
alogin().
-
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()с помощью использования более слабого хешера во время тестирования.Изменено в Django 5.0:Добавлен метод
aforce_login().
-
logout()
-
alogout() -
Асинхронная версия:
alogout()Если ваш сайт использует систему аутентификации Django, метод
logout()можно использовать для моделирования выхода пользователя из вашего сайта.После вызова этого метода, тестовый клиент очистит все куки и данные сессии до значений по умолчанию. Следующие запросы будут поступать от
AnonymousUser.Изменено в Django 5.0:Добавлен метод
alogout().
-
Тестирование ответов
Методы 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's
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 для предоставления информации об обработанном исключении.
Состояние сохранения
Тестовый клиент имеет состояние. Если ответ возвращает куки, то эти куки будут сохранены в тестовом клиенте и отправлены со всеми последующими get() и post() запросами.
Политики истечения срока действия этих куки не следуют. Если вы хотите, чтобы куки истекли, удалите его вручную или создайте новый экземпляр Client (что эффективно удалит все куки).
У тестового клиента есть атрибуты, которые хранят информацию о состоянии сохранения. Вы можете получить доступ к этим свойствам в рамках условия теста.
-
Client.cookies -
Объект Python
SimpleCookie, содержащий текущие значения всех куки клиента. См. документацию модуляhttp.cookiesдля получения подробной информации.
-
Client.session -
Объект, подобный словарю, содержащий информацию о сеансе. Полные детали см. в документации по сеансам.
Для изменения сеанса и его сохранения, необходимо сначала сохранить его в переменной (потому что новый
SessionStoreсоздаётся каждый раз при обращении к этому свойству):def test_something(self): session = self.client.session session["somekey"] = "test" session.save()
-
Client.asession() -
Новое в Django 5.0.
Это аналогично атрибуту
session, но работает в асинхронных контекстах.
Установка языка
При тестировании приложений, поддерживающих интернационализацию и локализация, вы, возможно, захотите установить язык для запроса тестового клиента. Способ установки зависит от того, включён ли LocaleMiddleware.
Если middleware включён, язык можно установить, создав куки с именем 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.")
Дополнительные сведения см. в Явное задание активного языка.
Пример
Ниже приведенный юнит-тест использует тестовый клиент:
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)
См. также
Предоставленные классы тестовых случаев
Обычные классы юнит-тестов Python наследуют от базового класса unittest.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на равенство.
- Проверка, что вызываемый объект
- Возможность запуска тестов с измененными настройками.
- Использование
clientClient.
Если ваши тесты выполняют запросы к базе данных, используйте подклассы 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 идентичны, за исключением способа сброса базы данных до известного состояния и возможности кода теста проверять влияние команд commit и rollback:
TransactionTestCaseсбрасывает базу данных после выполнения теста, обнуляя все таблицы.TransactionTestCaseможет вызывать commit и rollback и наблюдать влияние этих вызовов на базу данных.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.8.0"
...\> py -m pip install "selenium >= 4.8.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 получил ответ, и что следующая страница загрузилась, прежде чем продолжить дальнейшее выполнение теста. Сделайте это, например, заставив 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) -
Очистка тестового ящика вывода
Если вы используете любые из пользовательских классов Django TestCase, тестовый запуск очистит содержимое тестового ящика вывода электронной почты в начале каждого тестового случая.
Более подробная информация об услугах электронной почты во время тестов приведена ниже в разделе Услуги электронной почты.
Утверждения
Поскольку стандартный класс 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содержится в сообщении об исключении. Любой другой результат будет reported как ошибка. Это упрощённая версия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().Устарело начиная с версии 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.
-
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>'world'!</p>", """<p> Hello <b>'world'! </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
Вы также можете исключить тесты по тегу. Чтобы запустить тесты ядра, если они не медленные:
$ ./manage.py test --tag=core --exclude-tag=slow
...\> manage.py test --tag=core --exclude-tag=slow
test --exclude-tag имеет приоритет над test --tag, поэтому, если тест имеет два тега, и вы выбираете один из них, а исключаете другой, тест не будет запущен.
Тестирование асинхронного кода
Если вы просто хотите протестировать вывод ваших асинхронных представлений, стандартный тестовый клиент запустит их внутри собственного асинхронного цикла без дополнительных усилий с вашей стороны.
Однако, если вы хотите написать полностью асинхронные тесты для проекта Django, вам нужно учитывать несколько моментов.
END_OF_DOCUMENT_MARKERВо-первых, ваши тесты должны быть методами класса теста (чтобы предоставить им контекст асинхронности). Django автоматически обнаружит любые асинхронные тесты и обернёт их так, чтобы они выполнялись в собственном цикле событий.
Если вы тестируете из асинхронной функции, вы также должны использовать асинхронный клиент тестирования. Он доступен как django.test.AsyncClient, или как self.async_client в любом тесте.
-
class AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, **defaults)
AsyncClient имеет те же методы и сигнатуры, что и синхронный (обычный) клиент тестирования, за исключением следующих:
- При инициализации произвольные ключевые аргументы в
defaultsдобавляются напрямую в ASGI-области. -
Заголовки, переданные в качестве
extraключевых аргументов, не должны иметь префиксHTTP_, необходимый для синхронного клиента (см.Client.get()). Например, вот как установить заголовок HTTPAccept:>>> c = AsyncClient() >>> c.get("/customers/details/", {"name": "fred", "age": 7}, ACCEPT="application/json")
Добавлен параметр headers.
В AsyncClient добавлен параметр поддержки follow.
Используя 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", poll_ids=[1], stdout=out)
self.assertIn('Successfully closed poll "1"', 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/5.0/topics/testing/tools/