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

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 and extra keyword arguments passed to get(), 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 в заголовке HTTP Content-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 and attachment, the latter a 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 (чтение двоичных данных).

The headers and extra parameters acts the same as for 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:

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 a Response object. This method works just like Client.get(), including the follow, secure, headers, and extra 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, and extra parameters act the same as for Client.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, and extra parameters act the same as for Client.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, and extra parameters act the same as for Client.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, and extra parameters act the same as for Client.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, and extra parameters act the same as for Client.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)

См.также

django.test.RequestFactory

Предоставленные классы тестовых примеров

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

Hierarchy of Django unit testing classes (TestCase subclasses)

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

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

SimpleTestCase

class SimpleTestCase[исходный код]

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

Если ваши тесты делают какие-либо запросы к базе данных, используйте подклассы 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 сбрасывает базу данных после выполнения теста, усекая все таблицы. A TransactionTestCase может вызывать фиксацию и откат и наблюдать за влиянием этих вызовов на базу данных.
  • С другой стороны, 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 with copy.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 named birds. 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’s non-field errors, use field=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 a FormSet instance. The formset must be bound but not necessarily validated (assertFormSetError() will automatically call the full_clean() on the formset).

form_index is the number of the form within the FormSet (starting from 0). Use form_index=None to check the formset’s non-form errors, i.e. the errors you get when calling formset.non_form_errors(). In that case you must also use field=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 given status_code and that text appears in its content. If count is provided, text must occur exactly count 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 given status_code and that text does not appear in its content.

Установите 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 is None, 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 a status_code redirect status, redirected to expected_url (including any GET data), and that the final page was received with 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 не имеет значения.
  • 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>&#x27;world&#x27;!</p>',
    '''<p>
        Hello   <b>&#39;world&#39;! </b>
    </p>'''
)
self.assertHTMLEqual(
    '<input type="checkbox" checked="checked" id="id_accept_terms" />',
    '<input id="id_accept_terms" type="checkbox" checked>'
)

html1 and html2 must contain HTML. An AssertionError will be raised if one of them cannot be parsed.

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

SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None)[исходный код]

Утверждает, что строки html1 и html2 не равны. Сравнение основано на семантике HTML. Подробности см. в assertHTMLEqual().

html1 and html2 must contain HTML. An AssertionError will be raised if one of them cannot be parsed.

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

SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None)[исходный код]

Asserts that the strings xml1 and xml2 are equal. The comparison is based on XML semantics. Similarly to assertHTMLEqual(), the comparison is made on parsed content, hence only semantic differences are considered, not syntax differences. When invalid XML is passed in any parameter, an AssertionError 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 the haystack 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 values values.

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

By default, the comparison is also ordering dependent. If qs doesn’t provide an implicit ordering, you can set the ordered parameter to False, which turns the comparison into a collections.Counter comparison. If the order is undefined (if the given qs isn’t ordered and the comparison is against more than one ordered value), a ValueError 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()). Например, вот как установить заголовок HTTP Accept:

    >>> c = AsyncClient()
    >>> c.get(
    ...     '/customers/details/',
    ...     {'name': 'fred', 'age': 7},
    ...     ACCEPT='application/json'
    ... )
    
Changed in Django 4.2:

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
Вернуться на верх