Инструменты для тестирования¶
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 environ variables. Например, чтобы задать имя скрипта: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
, вы можете вызвать любой из следующих методов:Changed in 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 environ variables. Например, заголовки для задания имени скрипта:
>>> c = Client() >>> c.get("/", SCRIPT_NAME="/app/")
Если у вас уже есть аргументы GET в URL-кодировке, вы можете использовать эту кодировку вместо аргумента data. Например, предыдущий GET-запрос можно представить в виде:
>>> c = Client() >>> c.get("/customers/details/?name=fred&age=7")
Если вы предоставляете URL с закодированными данными GET и аргументом data, аргумент data будет иметь приоритет.
Если вы установите
follow
вTrue
, клиент будет следовать любым перенаправлениям, а в объекте ответа будет установлен атрибутredirect_chain
, содержащий кортежи промежуточных адресов и кодов состояния.Если бы у вас был URL
/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.Changed in 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()
, если это dict, список или кортеж. По умолчанию сериализация выполняется с помощьюDjangoJSONEncoder
, и ее можно переопределить, предоставив аргументjson_encoder
дляClient
. Эта сериализация также происходит для запросовput()
,patch()
иdelete()
.Если вы предоставите любой другой
content_type
(например, text/xml для полезной нагрузки XML), содержимоеdata
будет отправлено как есть в POST-запросе, используяcontent_type
в заголовке HTTPContent-Type
.Если вы не укажете значение для
content_type
, значения вdata
будут переданы с типом содержимого multipart/form-data. В этом случае пары ключ-значение вdata
будут закодированы как многокомпонентное сообщение и использованы для создания полезной нагрузки данных POST.Чтобы отправить несколько значений для заданного ключа - например, чтобы указать выбранные значения для поля
<select multiple>
- предоставьте значения в виде списка или кортежа для требуемого ключа. Например, значениеdata
представит три выбранных значения для поля с именемchoices
:{"choices": ["a", "b", "d"]}
Отправка файлов - это особый случай. Чтобы отправить файл, достаточно указать в качестве ключа имя поля file, а в качестве значения - handle файла, который вы хотите загрузить. Например, если в вашей форме есть поля
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
, содержащий кортежи промежуточных адресов и кодов состояния.Если вы установите
secure
вTrue
, клиент будет эмулировать запрос HTTPS.Changed in Django 4.2:Добавлен параметр
headers
.
-
head
(path, data=None, follow=False, secure=False, *, headers=None, **extra)[исходный код]¶ Выполняет HEAD-запрос на предоставленном
path
и возвращает объектResponse
. Этот метод работает так же, какClient.get()
, включая параметрыfollow
,secure
,headers
иextra
, за исключением того, что он не возвращает тело сообщения.Changed in 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()
.Changed in 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()
.Changed in 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()
.Changed in 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()
.Changed in 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()
.Changed in Django 4.2:Добавлен параметр
headers
.
-
login
(**credentials)¶ Если ваш сайт использует authentication system Django и вы имеете дело с регистрацией пользователей, вы можете использовать метод
login()
тестового клиента для имитации эффекта входа пользователя на сайт.После вызова этого метода тестовый клиент будет иметь все куки и данные сессии, необходимые для прохождения любых тестов на основе входа в систему, которые могут быть частью представления.
Формат аргумента
credentials
зависит от того, какой authentication backend вы используете (что определяется настройками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)¶ Если ваш сайт использует authentication system Django, вы можете использовать метод
force_login()
для имитации эффекта входа пользователя на сайт. Используйте этот метод вместоlogin()
, когда тест требует, чтобы пользователь вошел в систему, а детали того, как пользователь вошел в систему, не важны.В отличие от
login()
, этот метод пропускает этапы аутентификации и проверки: неактивным пользователям (is_active=False
) разрешено входить в систему, а учетные данные пользователя предоставлять не нужно.Атрибут пользователя
backend
будет установлен на значение аргументаbackend
(который должен быть точечной строкой пути Python), или наsettings.AUTHENTICATION_BACKENDS[0]
, если значение не предоставлено. Функцияauthenticate()
, вызываемаяlogin()
, обычно аннотирует пользователя следующим образом.Этот метод быстрее, чем
login()
, поскольку обходятся дорогостоящие алгоритмы хэширования паролей. Кроме того, вы можете ускоритьlogin()
на using a weaker hasher while testing.
-
logout
()¶ Если ваш сайт использует Django authentication system, метод
logout()
может быть использован для имитации эффекта выхода пользователя из сайта.После вызова этого метода у тестового клиента все cookies и данные сессии будут очищены до значений по умолчанию. Последующие запросы будут выглядеть как исходящие от
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()
. Их значения следующие:- тип: Тип исключения.
- значение: Экземпляр исключения.
- traceback: Объект 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 status code registry.
-
templates
¶ Список шаблонов
Template
, используемых для отображения конечного содержимого, в порядке их отображения. Для каждого шаблона в списке используйтеtemplate.name
, чтобы получить имя файла шаблона, если шаблон был загружен из файла. (Имя представляет собой строку, например'admin/index.html'
).Не используете шаблоны Django?
Этот атрибут заполняется только при использовании бэкенда
DjangoTemplates
. Если вы используете другой шаблонизатор,template_name
может быть подходящей альтернативой, если вам нужно только имя шаблона, используемого для рендеринга.
-
resolver_match
¶ Экземпляр
ResolverMatch
для ответа. Вы можете использовать атрибутfunc
, например, для проверки представления, обслужившего ответ:# my_view here is a function based view. self.assertEqual(response.resolver_match.func, my_view) # Class-based views need to compare the view_class, as the # functions generated by as_view() won't be equal. self.assertIs(response.resolver_match.func.view_class, MyView)
Если заданный URL не найден, обращение к этому атрибуту вызовет исключение
Resolver404
.
-
Как и в случае с обычным ответом, вы также можете получить доступ к заголовкам через HttpResponse.headers
. Например, можно определить тип содержимого ответа с помощью response.headers['Content-Type']
.
Исключения¶
Если вы направите тестовый клиент на представление, которое вызывает исключение, и Client.raise_request_exception
будет True
, это исключение будет видно в тестовом примере. Затем вы можете использовать стандартный блок try ... except
или assertRaises()
для проверки исключений.
Единственными исключениями, которые не видны тестовому клиенту, являются Http404
, PermissionDenied
, SystemExit
и SuspiciousOperation
. Django перехватывает эти исключения внутренне и преобразует их в соответствующие коды ответов HTTP. В этих случаях вы можете проверить response.status_code
в вашем тесте.
Если Client.raise_request_exception
равно False
, тестовый клиент вернет ответ 500, как это было бы в браузере. Ответ имеет атрибут exc_info
для предоставления информации о необработанном исключении.
Постоянное состояние¶
Тестовый клиент является государственным. Если ответ возвращает cookie, то это cookie будет сохранено в тестовом клиенте и отправлено со всеми последующими запросами get()
и post()
.
Политика истечения срока действия этих файлов cookie не соблюдается. Если вы хотите, чтобы срок действия cookie истек, либо удалите его вручную, либо создайте новый экземпляр Client
(что приведет к эффективному удалению всех cookie).
Клиент теста имеет атрибуты, которые хранят постоянную информацию о состоянии. Вы можете получить доступ к этим свойствам как часть условия теста.
-
Client.
cookies
¶ Объект Python
SimpleCookie
, содержащий текущие значения всех клиентских cookies. Подробнее см. документацию модуляhttp.cookies
.
-
Client.
session
¶ Словарно-подобный объект, содержащий информацию о сеансе. Подробную информацию см. в session documentation.
Чтобы изменить сессию и затем сохранить ее, ее необходимо сначала сохранить в переменной (потому что при каждом обращении к этому свойству создается новое
SessionStore
):def test_something(self): session = self.client.session session["somekey"] = "test" session.save()
Настройка языка¶
При тестировании приложений, поддерживающих интернационализацию и локализацию, вам может понадобиться установить язык для запроса тестового клиента. Метод для этого зависит от того, включен или нет параметр 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)
См.также
Предоставленные классы тестовых примеров¶
Обычные классы модульных тестов 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
на равенство.
- Проверка того, что вызываемый объект
- Возможность запускать тесты с modified settings.
- Используя
client
Client
.
Если ваши тесты делают какие-либо запросы к базе данных, используйте подклассы TransactionTestCase
или TestCase
.
-
SimpleTestCase.
databases
¶ SimpleTestCase
запрещает запросы к базе данных по умолчанию. Это помогает избежать выполнения запросов на запись, которые повлияют на другие тесты, поскольку каждыйSimpleTestCase
тест не выполняется в транзакции. Если вас не беспокоит эта проблема, вы можете отключить это поведение, установив атрибутdatabases
class в'__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
. - Тест skipping based on database backend features.
- Остальные специализированные методы
assert*
.
Класс Django TestCase
является более часто используемым подклассом класса TransactionTestCase
, который использует средства транзакций базы данных для ускорения процесса сброса базы данных в известное состояние в начале каждого теста. Следствием этого, однако, является то, что некоторые поведения базы данных не могут быть протестированы в классе Django TestCase
. Например, вы не можете проверить, что блок кода выполняется в рамках транзакции, как это требуется при использовании select_for_update()
. В таких случаях следует использовать TransactionTestCase
.
TransactionTestCase
и TestCase
идентичны, за исключением способа сброса базы данных в известное состояние и возможности для тестового кода проверить эффекты фиксации и отката:
- Вариант
TransactionTestCase
сбрасывает базу данных после выполнения теста, усекая все таблицы. ATransactionTestCase
может вызывать фиксацию и откат и наблюдать за влиянием этих вызовов на базу данных. - С другой стороны,
TestCase
не усекает таблицы после теста. Вместо этого он заключает тестовый код в транзакцию базы данных, которая откатывается в конце теста. Это гарантирует, что откат в конце теста восстановит базу данных в исходное состояние.
Предупреждение
TestCase
, запущенный на базе данных, которая не поддерживает откат (например, MySQL с механизмом хранения MyISAM), и все экземпляры TransactionTestCase
, откатятся в конце теста, удалив все данные из тестовой базы данных.
Apps will not see their data reloaded; если вам нужна эта функциональность (например, сторонние приложения должны включить ее), вы можете установить 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 dummy client, такие как, например, клиент Selenium, для выполнения серии функциональных тестов внутри браузера и имитации действий реального пользователя.
Живой сервер слушает на localhost
и привязывается к порту 0, который использует свободный порт, назначенный операционной системой. Во время тестов доступ к URL сервера можно получить с помощью self.live_server_url
.
Чтобы продемонстрировать, как использовать LiveServerTestCase
, напишем Selenium-тест. Прежде всего, необходимо установить пакет selenium:
$ python -m pip install "selenium >= 3.8.0"
...\> py -m pip install "selenium >= 3.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; для получения более подробной информации ознакомьтесь с full reference.
Примечание
При использовании базы данных in-memory SQLite для запуска тестов одно и то же соединение с базой данных будет использоваться параллельно двумя потоками: потоком, в котором запускается живой сервер, и потоком, в котором запускается тестовый пример. Важно предотвратить одновременные запросы к базе данных через это общее соединение двумя потоками, так как это может привести к случайному сбою тестов. Поэтому вам нужно убедиться, что эти два потока не обращаются к базе данных в одно и то же время. В частности, это означает, что в некоторых случаях (например, сразу после нажатия на ссылку или отправки формы) вам может понадобиться проверить, что Selenium получил ответ и что следующая страница загружена, прежде чем приступать к дальнейшему выполнению теста. Сделать это можно, например, заставив 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 FAQ и Selenium documentation для получения дополнительной информации.
Особенности тестовых случаев¶
Тестовый клиент по умолчанию¶
-
SimpleTestCase.
client
¶
Каждый тестовый пример в экземпляре django.test.*TestCase
имеет доступ к экземпляру тестового клиента Django. Доступ к этому клиенту можно получить по адресу self.client
. Этот клиент создается заново для каждого теста, поэтому вам не нужно беспокоиться о том, что состояние (например, cookies) будет переноситься из одного теста в другой.
Это означает, что вместо инстанцирования Client
в каждом test:
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()
, однако можно использовать и fixtures.
Фикстура - это набор данных, которые Django умеет импортировать в базу данных. Например, если на вашем сайте есть учетные записи пользователей, вы можете создать фикстуру из поддельных учетных записей пользователей, чтобы заполнить базу данных во время тестирования.
Самым простым способом создания приспособления является использование команды manage.py dumpdata
. Это предполагает, что у вас уже есть некоторые данные в вашей базе данных. Для получения более подробной информации см. команду dumpdata documentation
.
После создания фикстуры и размещения ее в каталоге fixtures
в одном из ваших INSTALLED_APPS
, вы можете использовать ее в ваших модульных тестах, указав атрибут fixtures
class в вашем django.test.TestCase
subclass:
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
class на тестовом классе, чтобы запросить обертывание транзакций против не``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 | Часовой пояс баз данных |
ТЕМПЛАТЫ | Шаблонные двигатели |
МОДУЛИ СЕРИАЛИЗАЦИИ | Кэш сериализаторов |
ЛОКАЛЬНЫЕ_ПУТИ, ЯЗЫКОВОЙ_КОД | Перевод по умолчанию и загруженные переводы |
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
, программа запуска тестов будет очищать содержимое тестового почтового ящика в начале каждого тестового случая.
Более подробно об услугах электронной почты во время тестирования см. ниже Email services.
Утверждения¶
Поскольку обычный класс 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
, возвращает менеджер контекста, так что тестируемый код может быть написан inline, а не как функция: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 – args, переданные для инстанцирования поля.
- field_kwargs – 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
. Форма должна быть bound, но не обязательно валидирована (assertFormError()
автоматически вызоветfull_clean()
на форме).field
- это имя поля формы, которое нужно проверить. Чтобы проверитьnon-field errors
, используйтеfield=None
.errors
- это список всех строк ошибок, которые ожидаются в данном поле. Вы также можете передать одну строку ошибок, если вы ожидаете только одну ошибку, что означает, чтоerrors='error message'
будет то же самое, что иerrors=['error message']
.Changed in Django 4.1:В старых версиях использование пустого списка ошибок с помощью
assertFormError()
всегда проходило, независимо от того, были ли ошибки в поле или нет. Начиная с Django 4.1, использованиеerrors=[]
будет проходить только в том случае, если поле действительно не имеет ошибок.Django 4.1 также изменил поведение
assertFormError()
, когда поле имеет несколько ошибок. В старых версиях, если поле имело несколько ошибок, а вы проверяли только некоторые из них, тест проходил. Начиная с Django 4.1, список ошибок должен точно соответствовать фактическим ошибкам поля.Не рекомендуется, начиная с версии 4.1: Поддержка передачи объекта ответа и имени формы в
assertFormError()
устарела и будет удалена в Django 5.0. Вместо этого используйте непосредственно экземпляр формы.
-
SimpleTestCase.
assertFormSetError
(formset, form_index, field, errors, msg_prefix='')[исходный код]¶ Утверждает, что
formset
при отображении вызывает указанный список ошибок.formset
является экземпляромFormSet
. Набор форм должен быть связан, но не обязательно подтвержден (assertFormSetError()
автоматически вызоветfull_clean()
на наборе форм).form_index
- номер формы внутриFormSet
(начиная с 0). Для проверки неформенных ошибок набора форм, т.е. ошибок, возникающих при вызовеformset.non_form_errors()
, используйтеform_index=None
. В этом случае необходимо также использоватьfield=None
.field
иerrors
имеют то же значение, что и параметры вassertFormError()
.Не рекомендуется, начиная с версии 4.1: Поддержка передачи объекта ответа и имени набора форм в
assertFormSetError()
является устаревшей и будет удалена в Django 5.0. Вместо этого используйте непосредственно экземпляр набора форм.Не рекомендуется, начиная с версии 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
, что превратит сравнение в сравнениеcollections.Counter
. Если порядок не определен (если данное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, вам нужно будет принять во внимание несколько вещей.
Во-первых, ваши тесты должны быть async def
методами на тестовом классе (для того, чтобы дать им асинхронный контекст). Django автоматически обнаружит любые async def
тесты и обернет их так, чтобы они выполнялись в собственном цикле событий.
Если вы проводите тестирование из асинхронной функции, вы также должны использовать клиент асинхронного тестирования. Он доступен как django.test.AsyncClient
или как self.async_client
в любом тесте.
-
class
AsyncClient
(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, **defaults)[исходный код]¶
AsyncClient
имеет те же методы и сигнатуры, что и синхронный (обычный) тестовый клиент, за следующими исключениями:
При инициализации произвольные аргументы ключевых слов в
defaults
добавляются непосредственно в область видимости ASGI.Параметр
follow
не поддерживается.Заголовки, передаваемые в качестве аргументов ключевого слова
extra
, не должны иметь префиксаHTTP_
, требуемого синхронным клиентом (см.Client.get()
). Например, здесь показано, как установить заголовок HTTPAccept
:>>> c = AsyncClient() >>> c.get("/customers/details/", {"name": "fred", "age": 7}, ACCEPT="application/json")
Добавлен параметр headers
.
При использовании AsyncClient
любой метод, выполняющий запрос, должен быть ожидаемым:
async def test_my_thing(self):
response = await self.async_client.get("/some-url/")
self.assertEqual(response.status_code, 200)
Асинхронный клиент может также вызывать синхронные представления; он запускается через asynchronous request path Django, который поддерживает оба варианта. Любое представление, вызванное через AsyncClient
, получит объект ASGIRequest
для своего request
, а не WSGIRequest
, который создает обычный клиент.
Предупреждение
Если вы используете тестовые декораторы, они должны быть async-совместимыми, чтобы гарантировать их корректную работу. Встроенные в 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’s email functionality, вы, вероятно, не хотите отправлять электронную почту каждый раз, когда запускаете тест, использующий это представление. По этой причине бегунок тестирования 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")
Как было отмечено previously, тестовый outbox опустошается в начале каждого теста в Django *TestCase
. Чтобы опустошить папку outbox вручную, назначьте пустой список на mail.outbox
:
from django.core import mail
# Empty the test outbox
mail.outbox = []
Команды управления¶
Команды управления могут быть проверены с помощью функции call_command()
. Вывод может быть перенаправлен в экземпляр StringIO
:
from io import StringIO
from django.core.management import call_command
from django.test import TestCase
class ClosepollTest(TestCase):
def test_command_output(self):
out = StringIO()
call_command("closepoll", stdout=out)
self.assertIn("Expected output", out.getvalue())
Пропуск тестов¶
Библиотека unittest предоставляет декораторы @skipIf
и @skipUnless
, позволяющие пропускать тесты, если вы заранее знаете, что эти тесты не пройдут при определенных условиях.
Например, если для успешного выполнения вашего теста требуется определенная дополнительная библиотека, вы можете украсить тестовый пример символом @skipIf
. Тогда программа запуска тестов сообщит, что тест не был выполнен и почему, вместо того чтобы выдать ошибку или вообще пропустить тест.
Чтобы дополнить это поведение пропуска тестов, Django предоставляет два дополнительных декоратора пропуска. Вместо проверки общего булева числа эти декораторы проверяют возможности базы данных и пропускают тест, если база данных не поддерживает определенную функцию.
Декораторы используют строковый идентификатор для описания особенностей базы данных. Эта строка соответствует атрибутам класса особенностей подключения к базе данных. Полный список особенностей базы данных, которые могут быть использованы в качестве основы для пропуска тестов, см. в django.db.backends.base.features.BaseDatabaseFeatures class.
-
skipIfDBFeature
(*feature_name_strings)[исходный код]¶
Пропустите тест на декорирование или TestCase
, если все названные функции базы данных поддерживаются.
Например, следующий тест не будет выполнен, если база данных поддерживает транзакции (например, он не будет выполняться в PostgreSQL, но будет выполняться в MySQL с таблицами MyISAM):
class MyTests(TestCase):
@skipIfDBFeature("supports_transactions")
def test_transaction_behavior(self):
# ... conditional test code
pass
-
skipUnlessDBFeature
(*feature_name_strings)[исходный код]¶
Пропустите тест на декорирование или TestCase
, если какая-либо из названных функций базы данных не поддерживается.
Например, следующий тест будет выполнен, только если база данных поддерживает транзакции (например, он будет выполняться в PostgreSQL, но не в MySQL с таблицами MyISAM):
class MyTests(TestCase):
@skipUnlessDBFeature("supports_transactions")
def test_transaction_behavior(self):
# ... conditional test code
pass