Инструменты для тестирования¶
Django предоставляет небольшой набор инструментов, которые могут пригодиться при написании тестов.
Тестовый клиент¶
The test client is a Python class that acts as a dummy web browser, allowing you to test your views and interact with your Django-powered application programmatically.
С помощью тестового клиента можно выполнять следующие действия:
- Моделируйте запросы GET и POST на URL и наблюдайте за ответом - все, от низкоуровневого HTTP (заголовки результатов и коды состояния) до содержимого страницы.
- Посмотрите цепочку перенаправлений (если таковые имеются) и проверьте URL и код состояния на каждом этапе.
- Проверьте, что заданный запрос отображается заданным шаблоном Django, с контекстом шаблона, содержащим определенные значения.
Обратите внимание, что тестовый клиент не предназначен для замены Selenium или других «внутрибраузерных» фреймворков. Тестовый клиент Django имеет другую направленность. Вкратце:
- Используйте тестовый клиент Django, чтобы убедиться, что отображается правильный шаблон и что шаблону передаются правильные контекстные данные.
- Используйте
RequestFactory
для тестирования функций представления напрямую, минуя уровни маршрутизации и промежуточного ПО. - Use in-browser frameworks like Selenium to test rendered HTML and the
behavior of web pages, namely JavaScript functionality. Django also
provides special support for those frameworks; see the section on
LiveServerTestCase
for more details.
A comprehensive test suite should use a combination of all of these test types.
Обзор и небольшой пример¶
To use the test client, instantiate django.test.Client
and retrieve
web pages:
>>> 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.
Обратите внимание на несколько важных моментов в работе тестового клиента:
The test client does not require the web server to be running. In fact, it will run just fine with no web server running at all! That’s because it avoids the overhead of HTTP and deals directly with the Django framework. This helps make the unit tests run quickly.
При получении страниц не забывайте указывать путь URL, а не весь домен. Например, правильно будет:
>>> c.get('/login/')
Это неверно:
>>> c.get('https://www.example.com/login/')
The test client is not capable of retrieving web pages that are not powered by your Django project. If you need to retrieve other web pages, use a Python standard library module such as
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
.The values from the
headers
andextra
keyword arguments passed toget()
,post()
, etc. have precedence over the defaults passed to the class constructor.Аргумент
enforce_csrf_checks
можно использовать для проверки защиты от CSRF (см. выше).Аргумент
raise_request_exception
позволяет контролировать, должны ли исключения, возникшие во время запроса, также возникать в тесте. По умолчанию установлено значениеTrue
.Аргумент
json_encoder
позволяет установить пользовательский JSON-кодер для сериализации JSON, описанной вpost()
.Когда у вас есть экземпляр
Client
, вы можете вызвать любой из следующих методов:Changed in Django 4.2:The
headers
parameter was added.-
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
The
headers
parameter can be used to specify headers to be sent in the request. For example:>>> 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:The
headers
parameter was added.
-
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']}
Submitting files is a special case. To POST a file, you need only provide the file field name as a key, and a file handle to the file you wish to upload as a value. For example, if your form has fields
name
andattachment
, the latter aFileField
:>>> 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
(чтение двоичных данных).The
headers
andextra
parameters acts the same as forClient.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:The
headers
parameter was added.
-
head
(path, data=None, follow=False, secure=False, *, headers=None, **extra)[исходный код]¶ Makes a HEAD request on the provided
path
and returns aResponse
object. This method works just likeClient.get()
, including thefollow
,secure
,headers
, andextra
parameters, except it does not return a message body.Changed in Django 4.2:The
headers
parameter was added.
-
options
(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)[исходный код]¶ Выполняет запрос OPTIONS на предоставленный
path
и возвращает объектResponse
. Используется для тестирования RESTful интерфейсов.Когда предоставляется
data
, он используется в качестве тела запроса, а заголовокContent-Type
устанавливается вcontent_type
.The
follow
,secure
,headers
, andextra
parameters act the same as forClient.get()
.Changed in Django 4.2:The
headers
parameter was added.
-
put
(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)[исходный код]¶ Выполняет запрос PUT на предоставленный
path
и возвращает объектResponse
. Полезно для тестирования RESTful интерфейсов.Когда предоставляется
data
, он используется в качестве тела запроса, а заголовокContent-Type
устанавливается вcontent_type
.The
follow
,secure
,headers
, andextra
parameters act the same as forClient.get()
.Changed in Django 4.2:The
headers
parameter was added.
-
patch
(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)[исходный код]¶ Выполняет запрос PATCH на предоставленный
path
и возвращает объектResponse
. Полезно для тестирования RESTful интерфейсов.The
follow
,secure
,headers
, andextra
parameters act the same as forClient.get()
.Changed in Django 4.2:The
headers
parameter was added.
-
delete
(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)[исходный код]¶ Делает запрос DELETE на предоставленный
path
и возвращает объектResponse
. Полезно для тестирования RESTful интерфейсов.Когда предоставляется
data
, он используется в качестве тела запроса, а заголовокContent-Type
устанавливается вcontent_type
.The
follow
,secure
,headers
, andextra
parameters act the same as forClient.get()
.Changed in Django 4.2:The
headers
parameter was added.
-
trace
(path, follow=False, secure=False, *, headers=None, **extra)[исходный код]¶ Делает запрос TRACE на предоставленный
path
и возвращает объектResponse
. Полезен для имитации диагностических зондов.Unlike the other request methods,
data
is not provided as a keyword parameter in order to comply with RFC 9110#section-9.3.8, which mandates that TRACE requests must not have a body.The
follow
,secure
,headers
, andextra
parameters act the same as forClient.get()
.Changed in Django 4.2:The
headers
parameter was added.
-
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
¶ Кортеж из трех значений, который предоставляет информацию о необработанном исключении, если таковое имело место, которое произошло во время просмотра.
Значения (тип, значение, 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
.
-
As with a normal response, you can also access the headers through
HttpResponse.headers
. For example, you could determine the content
type of a response using 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).
A test client has attributes that store persistent state information. You can access these properties as part of a test condition.
-
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('/', HTTP_ACCEPT_LANGUAGE='fr')
self.assertEqual(response.content, b"Bienvenue sur mon site.")
Более подробную информацию можно найти в Как 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
равны. - Verifying an HTTP
redirect
is performed by the app. - Надежное тестирование двух
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.
- The remaining specialized
assert*
methods.
Класс 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()
будет вызываться перед каждым тестом, сводя на нет преимущества в скорости.Objects assigned to class attributes in
setUpTestData()
must support creating deep copies withcopy.deepcopy()
in order to isolate them from alterations performed by each test methods.
-
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 package в ваш путь к Python:
$ python -m pip install selenium
...\> py -m pip install selenium
Затем добавьте тест на основе LiveServerTestCase
в модуль тестов вашего приложения (например: myapp/tests.py
). В этом примере мы предположим, что вы используете приложение staticfiles
и хотите, чтобы статические файлы обслуживались во время выполнения ваших тестов аналогично тому, что мы получаем во время разработки с помощью DEBUG=True
, т.е. без необходимости собирать их с помощью collectstatic
. Мы будем использовать подкласс StaticLiveServerTestCase
, который обеспечивает эту функциональность. Замените его на django.test.LiveServerTestCase
, если вам это не нужно.
Код для этого теста может выглядеть следующим образом:
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver
class MySeleniumTests(StaticLiveServerTestCase):
fixtures = ['user-data.json']
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.selenium = WebDriver()
cls.selenium.implicitly_wait(10)
@classmethod
def tearDownClass(cls):
cls.selenium.quit()
super().tearDownClass()
def test_login(self):
self.selenium.get(f'{self.live_server_url}/login/')
username_input = self.selenium.find_element(By.NAME, "username")
username_input.send_keys('myuser')
password_input = self.selenium.find_element(By.NAME, "password")
password_input.send_keys('secret')
self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()
Наконец, вы можете запустить тест следующим образом:
$ ./manage.py test myapp.tests.MySeleniumTests.test_login
...\> manage.py test myapp.tests.MySeleniumTests.test_login
В этом примере автоматически откроется Firefox, затем перейдите на страницу входа, введите учетные данные и нажмите кнопку «Войти». Selenium предлагает другие драйверы на случай, если у вас не установлен Firefox или вы хотите использовать другой браузер. Приведенный выше пример - лишь малая часть того, что может делать клиент Selenium; для получения более подробной информации ознакомьтесь с 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'))
The tricky thing here is that there’s really no such thing as a «page load,»
especially in modern web apps that generate HTML dynamically after the
server generates the initial document. So, checking for the presence of
<body>
in the response might not necessarily be appropriate for all use
cases. Please refer to the Selenium FAQ and Selenium documentation
for more information.
Особенности тестовых случаев¶
Тестовый клиент по умолчанию¶
-
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
¶
A test case for a database-backed website isn’t much use if there isn’t any
data in the database. Tests are more readable and it’s more maintainable to
create objects using the ORM, for example in TestCase.setUpTestData()
,
however, you can also use 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
. - Then, all the named fixtures are installed. In this example, Django will
install any JSON fixture named
mammals
, followed by any fixture namedbirds
. See the Приспособления topic for more details on defining and installing fixtures.
По соображениям производительности 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/')
This example will override the LOGIN_URL
setting for the code
in the with
block and reset its value to the previous state afterward.
-
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 | Часовой пояс баз данных |
ТЕМПЛАТЫ | Шаблонные двигатели |
МОДУЛИ СЕРИАЛИЗАЦИИ | Кэш сериализаторов |
ЛОКАЛЬНЫЕ_ПУТИ, ЯЗЫКОВОЙ_КОД | Перевод по умолчанию и загруженные переводы |
MEDIA_ROOT, DEFAULT_FILE_STORAGE | Хранилище файлов по умолчанию |
Изолирующие приложения¶
-
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 ...
The decorator form can also be applied to classes.
Можно указать два необязательных аргумента в виде ключевых слов:
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.
Утверждения¶
As Python’s normal unittest.TestCase
class implements assertion methods
such as assertTrue()
and
assertEqual()
, Django’s custom TestCase
class
provides a number of custom assertion methods that are useful for testing web
applications:
Сообщения о сбоях, выдаваемые большинством этих методов утверждения, можно настроить с помощью аргумента 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='')[исходный код]¶ Asserts that a field on a form raises the provided list of errors.
form
является экземпляромForm
. Форма должна быть bound, но не обязательно валидирована (assertFormError()
автоматически вызоветfull_clean()
на форме).field
is the name of the field on the form to check. To check the form’snon-field errors
, usefield=None
.errors
- это список всех строк ошибок, которые ожидаются в данном поле. Вы также можете передать одну строку ошибок, если вы ожидаете только одну ошибку, что означает, чтоerrors='error message'
будет то же самое, что иerrors=['error message']
.Changed in Django Development version:В старых версиях использование пустого списка ошибок с помощью
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
is aFormSet
instance. The formset must be bound but not necessarily validated (assertFormSetError()
will automatically call thefull_clean()
on the formset).form_index
is the number of the form within theFormSet
(starting from 0). Useform_index=None
to check the formset’s non-form errors, i.e. the errors you get when callingformset.non_form_errors()
. In that case you must also usefield=None
.field
иerrors
имеют то же значение, что и параметры вassertFormError()
.Не рекомендуется, начиная с версии 4.1: Support for passing a response object and a formset name to
assertFormSetError()
is deprecated and will be removed in Django 5.0. Use the formset instance directly instead.Не рекомендуется, начиная с версии 4.2: Метод утверждения
assertFormsetError()
устарел. Вместо него используйтеassertFormSetError()
.
-
SimpleTestCase.
assertContains
(response, text, count=None, status_code=200, msg_prefix='', html=False)[исходный код]¶ Asserts that a
response
produced the givenstatus_code
and thattext
appears in itscontent
. Ifcount
is provided,text
must occur exactlycount
times in the response.Установите
html
вTrue
, чтобы обрабатыватьtext
как HTML. Сравнение с содержимым ответа будет основано на семантике HTML, а не на равенстве символов. Пробельные символы в большинстве случаев игнорируются, упорядочивание атрибутов не имеет значения. Более подробную информацию смотрите вassertHTMLEqual()
.
-
SimpleTestCase.
assertNotContains
(response, text, status_code=200, msg_prefix='', html=False)[исходный код]¶ Asserts that a
response
produced the givenstatus_code
and thattext
does not appear in itscontent
.Установите
html
вTrue
, чтобы обрабатыватьtext
как HTML. Сравнение с содержимым ответа будет основано на семантике HTML, а не на равенстве символов. Пробельные символы в большинстве случаев игнорируются, упорядочивание атрибутов не имеет значения. Более подробную информацию смотрите вassertHTMLEqual()
.
-
SimpleTestCase.
assertTemplateUsed
(response, template_name, msg_prefix='', count=None)[исходный код]¶ Утверждает, что шаблон с заданным именем был использован при визуализации ответа.
response
должен быть экземпляром ответа, возвращаемымtest client
.template_name
should be a string such as'admin/index.html'
.The
count
argument is an integer indicating the number of times the template should be rendered. Default isNone
, meaning that the template should be rendered one or more times.Вы можете использовать его в качестве менеджера контекста, например, так:
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)[исходный код]¶ Asserts that the
response
returned astatus_code
redirect status, redirected toexpected_url
(including anyGET
data), and that the final page was received withtarget_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 не имеет значения.
- Boolean attributes (like
checked
) without an argument are equal to attributes that equal in name and value (see the examples). - Текст, ссылки на символы и ссылки на сущности, которые ссылаются на один и тот же символ, эквивалентны.
Следующие примеры являются корректными тестами и не вызывают никаких
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
andhtml2
must contain HTML. AnAssertionError
will be raised if one of them cannot be parsed.Вывод в случае ошибки может быть настроен с помощью аргумента
msg
.
-
SimpleTestCase.
assertHTMLNotEqual
(html1, html2, msg=None)[исходный код]¶ Утверждает, что строки
html1
иhtml2
не равны. Сравнение основано на семантике HTML. Подробности см. вassertHTMLEqual()
.html1
andhtml2
must contain HTML. AnAssertionError
will be raised if one of them cannot be parsed.Вывод в случае ошибки может быть настроен с помощью аргумента
msg
.
-
SimpleTestCase.
assertXMLEqual
(xml1, xml2, msg=None)[исходный код]¶ Asserts that the strings
xml1
andxml2
are equal. The comparison is based on XML semantics. Similarly toassertHTMLEqual()
, the comparison is made on parsed content, hence only semantic differences are considered, not syntax differences. When invalid XML is passed in any parameter, anAssertionError
is always raised, even if both strings are identical.Объявление XML, тип документа, инструкции по обработке и комментарии игнорируются. Сравниваются только корневой элемент и его дочерние элементы.
Вывод в случае ошибки может быть настроен с помощью аргумента
msg
.
-
SimpleTestCase.
assertXMLNotEqual
(xml1, xml2, msg=None)[исходный код]¶ Утверждает, что строки
xml1
иxml2
не равны. Сравнение основано на семантике XML. Подробности см. вassertXMLEqual()
.Вывод в случае ошибки может быть настроен с помощью аргумента
msg
.
-
SimpleTestCase.
assertInHTML
(needle, haystack, count=None, msg_prefix='')[исходный код]¶ Asserts that the HTML fragment
needle
is contained in thehaystack
once.Если указан целочисленный аргумент
count
, то дополнительно будет строго проверяться количество вхожденийneedle
.Whitespace in most cases is ignored, and attribute ordering is not significant. See
assertHTMLEqual()
for more details.
-
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)¶ Asserts that a queryset
qs
matches a particular iterable of valuesvalues
.Если указано
transform
,values
сравнивается со списком, полученным путем примененияtransform
к каждому членуqs
.By default, the comparison is also ordering dependent. If
qs
doesn’t provide an implicit ordering, you can set theordered
parameter toFalse
, which turns the comparison into acollections.Counter
comparison. If the order is undefined (if the givenqs
isn’t ordered and the comparison is against more than one ordered value), aValueError
is raised.Вывод в случае ошибки может быть настроен с помощью аргумента
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
has the same methods and signatures as the synchronous (normal)
test client, with two exceptions:
При инициализации произвольные аргументы ключевых слов в
defaults
добавляются непосредственно в область видимости ASGI.Параметр
follow
не поддерживается.Заголовки, передаваемые в качестве аргументов ключевого слова
extra
, не должны иметь префиксаHTTP_
, требуемого синхронным клиентом (см.Client.get()
). Например, вот как установить заголовок HTTPAccept
:>>> c = AsyncClient() >>> c.get( ... '/customers/details/', ... {'name': 'fred', 'age': 7}, ... ACCEPT='application/json' ... )
The headers
parameter was added.
При использовании 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 предоставляет два дополнительных декоратора пропуска. Вместо проверки общего булева числа эти декораторы проверяют возможности базы данных и пропускают тест, если база данных не поддерживает определенную функцию.
The decorators use a string identifier to describe database features. This string corresponds to attributes of the database connection features class. See django.db.backends.base.features.BaseDatabaseFeatures class for a full list of database features that can be used as a basis for skipping tests.
-
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