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

Django предоставляет небольшой набор инструментов, которые могут пригодиться при написании тестов.

Тестовый клиент

Тестовый клиент - это класс Python, который действует как фиктивный веб-браузер, позволяя вам тестировать ваши представления и взаимодействовать с вашим Django-приложением программно.

С помощью тестового клиента можно выполнять следующие действия:

  • Моделируйте запросы GET и POST на URL и наблюдайте за ответом - все, от низкоуровневого HTTP (заголовки результатов и коды состояния) до содержимого страницы.
  • Посмотрите цепочку перенаправлений (если таковые имеются) и проверьте URL и код состояния на каждом этапе.
  • Проверьте, что заданный запрос отображается заданным шаблоном Django, с контекстом шаблона, содержащим определенные значения.

Обратите внимание, что тестовый клиент не предназначен для замены Selenium или других «внутрибраузерных» фреймворков. Тестовый клиент Django имеет другую направленность. Вкратце:

  • Используйте тестовый клиент Django, чтобы убедиться, что отображается правильный шаблон и что шаблону передаются правильные контекстные данные.
  • Используйте внутрибраузерные фреймворки, такие как Selenium для тестирования рендеринга HTML и поведения веб-страниц, а именно функциональности JavaScript. Django также предоставляет специальную поддержку для этих фреймворков; подробнее см. раздел LiveServerTestCase.

Комплексный набор тестов должен использовать комбинацию обоих типов тестов.

Обзор и небольшой пример

Чтобы использовать тестовый клиент, инстанцируйте django.test.Client и получите веб-страницы:

>>> from django.test import Client
>>> c = Client()
>>> response = c.post('/login/', {'username': 'john', 'password': 'smith'})
>>> response.status_code
200
>>> response = c.get('/customer/details/')
>>> response.content
b'<!DOCTYPE html...'

Как следует из этого примера, вы можете инстанцировать Client из сеанса интерактивного интерпретатора Python.

Обратите внимание на несколько важных моментов в работе тестового клиента:

  • Тестовый клиент не требует, чтобы веб-сервер был запущен. На самом деле, он будет прекрасно работать и без веб-сервера! Это потому, что он избегает накладных расходов HTTP и работает напрямую с фреймворком Django. Это помогает быстро запускать модульные тесты.

  • При получении страниц не забывайте указывать путь URL, а не весь домен. Например, правильно будет:

    >>> c.get('/login/')
    

    Это неверно:

    >>> c.get('https://www.example.com/login/')
    

    Тестовый клиент не способен получать веб-страницы, которые не работают с вашим проектом Django. Если вам нужно получить другие веб-страницы, используйте модуль стандартной библиотеки Python, такой как urllib.

  • Для разрешения URL-адресов тестовый клиент использует тот URLconf, на который указывает ваша настройка ROOT_URLCONF.

  • Хотя приведенный выше пример будет работать в интерактивном интерпретаторе Python, некоторые функции тестового клиента, в частности, связанные с шаблонами, доступны только во время выполнения тестов.

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

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

    Если по какой-то причине вы хотите, чтобы тестовый клиент выполнял проверку CSRF, вы можете создать экземпляр тестового клиента, который будет выполнять проверку CSRF. Для этого передайте аргумент enforce_csrf_checks при создании клиента:

    >>> from django.test import Client
    >>> csrf_client = Client(enforce_csrf_checks=True)
    

Выполнение запросов

Используйте класс django.test.Client для выполнения запросов.

class Client(enforce_csrf_checks=False, json_encoder=DjangoJSONEncoder, **defaults)[исходный код]

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

>>> c = Client(HTTP_USER_AGENT='Mozilla/5.0')

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

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

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

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

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

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

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

Пары ключ-значение в словаре data используются для создания полезной нагрузки данных GET. Например:

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

…приведет к оценке GET-запроса, эквивалентного:

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

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

>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7},
...       HTTP_ACCEPT='application/json')

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

Спецификация CGI

Заголовки, передаваемые через **extra, должны соответствовать спецификации CGI. Например, эмуляция другого заголовка «Host», отправленного в HTTP-запросе от браузера к серверу, должна быть передана как HTTP_HOST.

Если у вас уже есть аргументы 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.

post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, **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')}

Отправка файлов - это особый случай. Чтобы отправить файл, достаточно указать имя поля file в качестве ключа, а в качестве значения - handle файла, который вы хотите загрузить. Например:

>>> c = Client()
>>> with open('wishlist.doc', 'rb') as fp:
...     c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})

(Имя attachment здесь не имеет значения; используйте любое имя, которое ожидает ваш код обработки файлов.)

Вы также можете предоставить любой файлоподобный объект (например, StringIO или BytesIO) в качестве дескриптора файла. Если вы загружаете на ImageField, то объект должен иметь name атрибут, который проходит validate_image_file_extension валидатор. Например:

>>> from io import BytesIO
>>> img = BytesIO(b'mybinarydata')
>>> img.name = 'myimage.jpg'

Обратите внимание, что если вы хотите использовать один и тот же дескриптор файла для нескольких вызовов post(), то вам нужно будет вручную сбрасывать указатель файла между вызовами. Самый простой способ сделать это - вручную закрыть файл после того, как он был предоставлен post(), как показано выше.

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

Аргумент extra действует так же, как и для Client.get().

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В отличие от других методов запроса, data не предоставляется в качестве параметра ключевого слова, чтобы соответствовать RFC 7231#section-4.3.8, который предписывает, что запросы TRACE не должны иметь тела.

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

login(**credentials)

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

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

Формат аргумента credentials зависит от того, какой authentication backend вы используете (который задается настройками AUTHENTICATION_BACKENDS). Если вы используете стандартный бэкенд аутентификации, предоставляемый Django (ModelBackend), credentials должны быть имя пользователя и пароль, предоставленные в качестве аргументов ключевых слов:

>>> c = Client()
>>> c.login(username='fred', password='secret')

# Now you can access a view that's only available to logged-in users.

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

login() возвращает True, если учетные данные были приняты и вход был успешным.

Наконец, вам нужно будет не забыть создать учетные записи пользователей, прежде чем вы сможете использовать этот метод. Как мы объяснили выше, программа запуска теста выполняется с использованием тестовой базы данных, которая по умолчанию не содержит пользователей. В результате учетные записи пользователей, действующие на вашем рабочем сайте, не будут работать в условиях тестирования. Вам нужно будет создать пользователей в рамках тестового пакета - либо вручную (используя API модели Django), либо с помощью тестового приспособления. Помните, что если вы хотите, чтобы у вашего тестового пользователя был пароль, вы не можете установить пароль пользователя, задав атрибут password напрямую - вы должны использовать функцию set_password() для хранения правильно хэшированного пароля. В качестве альтернативы вы можете использовать вспомогательный метод create_user() для создания нового пользователя с правильно хэшированным паролем.

force_login(user, backend=None)

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

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

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

Этот метод быстрее, чем login(), поскольку обходятся дорогостоящие алгоритмы хэширования паролей. Кроме того, вы можете ускорить login() на using a weaker hasher while testing.

logout()

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

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

Ответы на тестирование

Методы get() и post() оба возвращают объект Response. Этот объект Response не такой же, как объект HttpResponse, возвращаемый представлениями Django; объект тестового ответа имеет некоторые дополнительные данные, полезные для проверки тестовым кодом.

В частности, объект Response имеет следующие атрибуты:

class Response
client

Тестовый клиент, который был использован для выполнения запроса, в результате которого был получен ответ.

content

Тело ответа в виде байтовой строки. Это конечное содержимое страницы, отображаемое представлением, или любое сообщение об ошибке.

context

Экземпляр шаблона Context, который был использован для рендеринга шаблона, создавшего содержимое ответа.

Если на странице использовалось несколько шаблонов, то context будет список Context объектов, в том порядке, в котором они были отображены.

Независимо от количества шаблонов, используемых во время рендеринга, вы можете получить значения контекста с помощью оператора []. Например, контекстная переменная name может быть получена с помощью:

>>> response = client.get('/foo/')
>>> response.context['name']
'Arthur'

Не используете шаблоны Django?

Этот атрибут заполняется только при использовании бэкенда DjangoTemplates. Если вы используете другой шаблонизатор, context_data может быть подходящей альтернативой для ответов с этим атрибутом.

exc_info

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

Значения (type, value, traceback), те же, что возвращает Python sys.exc_info(). Их значения следующие:

  • тип: Тип исключения.
  • значение: Экземпляр исключения.
  • traceback: Объект traceback, который содержит стек вызовов в точке, где первоначально произошло исключение.

Если исключение не произошло, то exc_info будет None.

json(**kwargs)

Тело ответа, разобранное как JSON. Дополнительные аргументы в виде ключевых слов передаются в json.loads(). Например:

>>> response = client.get('/foo/')
>>> response.json()['name']
'Arthur'

Если заголовок Content-Type не "application/json", то при попытке разобрать ответ возникнет ошибка ValueError.

request

Данные запроса, которые стимулировали ответ.

wsgi_request

Экземпляр WSGIRequest, созданный обработчиком теста, который сгенерировал ответ.

status_code

HTTP-статус ответа, в виде целого числа. Полный список определенных кодов см. в IANA status code registry.

templates

Список шаблонов Template, используемых для отображения конечного содержимого, в порядке их отображения. Для каждого шаблона в списке используйте template.name, чтобы получить имя файла шаблона, если шаблон был загружен из файла. (Имя представляет собой строку, например 'admin/index.html').

Не используете шаблоны Django?

Этот атрибут заполняется только при использовании бэкенда DjangoTemplates. Если вы используете другой шаблонизатор, template_name может быть подходящей альтернативой, если вам нужно только имя шаблона, используемого для рендеринга.

resolver_match

Экземпляр ResolverMatch для ответа. Вы можете использовать атрибут func, например, для проверки представления, обслужившего ответ:

# my_view here is a function based view
self.assertEqual(response.resolver_match.func, my_view)

# class-based views need to be compared by name, as the functions
# generated by as_view() won't be equal
self.assertEqual(response.resolver_match.func.__name__, MyView.as_view().__name__)

Если заданный URL не найден, обращение к этому атрибуту вызовет исключение Resolver404.

Как и в случае с обычным ответом, вы также можете получить доступ к заголовкам через HttpResponse.headers. Например, можно определить тип содержимого ответа с помощью response.headers['Content-Type'].

Исключения

Если вы направите тестовый клиент на представление, которое вызывает исключение, и Client.raise_request_exception будет True, это исключение будет видно в тестовом примере. Затем вы можете использовать стандартный блок try ... except или assertRaises() для проверки исключений.

Единственными исключениями, которые не видны тестовому клиенту, являются Http404, PermissionDenied, SystemExit и SuspiciousOperation. Django перехватывает эти исключения внутренне и преобразует их в соответствующие коды ответов HTTP. В этих случаях вы можете проверить response.status_code в вашем тесте.

Если Client.raise_request_exception равно False, тестовый клиент вернет ответ 500, как это было бы в браузере. Ответ имеет атрибут exc_info для предоставления информации о необработанном исключении.

Постоянное состояние

Тестовый клиент является государственным. Если ответ возвращает cookie, то это cookie будет сохранено в тестовом клиенте и отправлено со всеми последующими запросами get() и post().

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

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

Client.cookies

Объект Python SimpleCookie, содержащий текущие значения всех клиентских cookies. Подробнее см. документацию модуля http.cookies.

Client.session

Словарно-подобный объект, содержащий информацию о сеансе. Подробную информацию см. в session documentation.

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

def test_something(self):
    session = self.client.session
    session['somekey'] = 'test'
    session.save()

Настройка языка

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

Если промежуточное ПО включено, язык может быть установлен путем создания cookie с именем LANGUAGE_COOKIE_NAME и значением кода языка:

from django.conf import settings

def test_language_using_cookie(self):
    self.client.cookies.load({settings.LANGUAGE_COOKIE_NAME: 'fr'})
    response = self.client.get('/')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

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

def test_language_using_header(self):
    response = self.client.get('/', 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.
  • Остальные специализированные методы assert*.

Класс 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() будет вызываться перед каждым тестом, сводя на нет преимущества в скорости.

Changed in Django 3.2:

Объекты, назначенные атрибутам класса в setUpTestData(), должны поддерживать создание глубоких копий с помощью copy.deepcopy() для того, чтобы изолировать их от изменений, выполняемых каждым тестовым методом. В предыдущих версиях Django эти объекты использовались повторно, и изменения, внесенные в них, сохранялись между тестовыми методами.

classmethod TestCase.captureOnCommitCallbacks(using=DEFAULT_DB_ALIAS, execute=False)[исходный код]
New in Django 3.2.

Возвращает менеджер контекста, который перехватывает 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')
Changed in Django 4.0:

В старых версиях новые обратные вызовы, добавленные во время выполнения обратных вызовов transaction.on_commit(), не перехватывались.

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.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('%s%s' % (self.live_server_url, '/login/'))
        username_input = self.selenium.find_element_by_name("username")
        username_input.send_keys('myuser')
        password_input = self.selenium.find_element_by_name("password")
        password_input.send_keys('secret')
        self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()

Наконец, вы можете запустить тест следующим образом:

$ ./manage.py test myapp.tests.MySeleniumTests.test_login
...\> manage.py test myapp.tests.MySeleniumTests.test_login

В этом примере автоматически откроется Firefox, затем перейдите на страницу входа, введите учетные данные и нажмите кнопку «Войти». Selenium предлагает другие драйверы на случай, если у вас не установлен Firefox или вы хотите использовать другой браузер. Приведенный выше пример - лишь малая часть того, что может делать клиент Selenium; для получения более подробной информации ознакомьтесь с full reference.

Примечание

При использовании базы данных in-memory SQLite для запуска тестов одно и то же соединение с базой данных будет использоваться параллельно двумя потоками: потоком, в котором запускается живой сервер, и потоком, в котором запускается тестовый пример. Важно предотвратить одновременные запросы к базе данных через это общее соединение двумя потоками, так как это может привести к случайному сбою тестов. Поэтому вам нужно убедиться, что эти два потока не обращаются к базе данных в одно и то же время. В частности, это означает, что в некоторых случаях (например, сразу после нажатия на ссылку или отправки формы) вам может понадобиться проверить, что Selenium получил ответ и что следующая страница загружена, прежде чем приступать к дальнейшему выполнению теста. Сделать это можно, например, заставив Selenium ждать, пока в ответе не будет найден HTML-тег <body> (требуется Selenium > 2.13):

def test_login(self):
    from selenium.webdriver.support.wait import WebDriverWait
    timeout = 2
    ...
    self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
    # Wait until the response is received
    WebDriverWait(self.selenium, timeout).until(
        lambda driver: driver.find_element_by_tag_name('body'))

Сложность здесь в том, что на самом деле не существует такого понятия, как «загрузка страницы», особенно в современных веб-приложениях, которые генерируют HTML динамически после того, как сервер создаст исходный документ. Поэтому проверка наличия <body> в ответе не всегда подходит для всех случаев использования. Пожалуйста, обратитесь к Selenium FAQ и Selenium documentation для получения дополнительной информации.

Особенности тестовых случаев

Тестовый клиент по умолчанию

SimpleTestCase.client

Каждый тестовый пример в экземпляре django.test.*TestCase имеет доступ к экземпляру тестового клиента Django. Доступ к этому клиенту можно получить по адресу self.client. Этот клиент создается заново для каждого теста, поэтому вам не нужно беспокоиться о том, что состояние (например, cookies) будет переноситься из одного теста в другой.

Это означает, что вместо инстанцирования Client в каждом test:

import unittest
from django.test import Client

class SimpleTest(unittest.TestCase):
    def test_details(self):
        client = Client()
        response = client.get('/customer/details/')
        self.assertEqual(response.status_code, 200)

    def test_index(self):
        client = Client()
        response = client.get('/customer/index/')
        self.assertEqual(response.status_code, 200)

…вы можете ссылаться на self.client, например, так:

from django.test import TestCase

class SimpleTest(TestCase):
    def test_details(self):
        response = self.client.get('/customer/details/')
        self.assertEqual(response.status_code, 200)

    def test_index(self):
        response = self.client.get('/customer/index/')
        self.assertEqual(response.status_code, 200)

Настройка клиента тестирования

SimpleTestCase.client_class

Если вы хотите использовать другой Client класс (например, подкласс с настроенным поведением), используйте атрибут client_class класса:

from django.test import Client, TestCase

class MyTestClient(Client):
    # Specialized methods for your environment
    ...

class MyTest(TestCase):
    client_class = MyTestClient

    def test_my_stuff(self):
        # Here self.client is an instance of MyTestClient...
        call_some_test_code()

Загрузка приспособлений

TransactionTestCase.fixtures

Тестовый пример для сайта с базой данных не имеет особого смысла, если в базе данных нет данных. Тесты более читабельны, и их удобнее поддерживать, если создавать объекты с помощью ORM, например, в TestCase.setUpTestData(), однако можно использовать и фикстуры.

Фикстура - это набор данных, которые Django умеет импортировать в базу данных. Например, если на вашем сайте есть учетные записи пользователей, вы можете создать фикстуру из поддельных учетных записей пользователей, чтобы заполнить базу данных во время тестирования.

Самым простым способом создания приспособления является использование команды manage.py dumpdata. Это предполагает, что у вас уже есть некоторые данные в вашей базе данных. Для получения более подробной информации см. команду dumpdata documentation.

После создания фикстуры и размещения ее в каталоге fixtures в одном из ваших INSTALLED_APPS, вы можете использовать ее в ваших модульных тестах, указав атрибут fixtures class в вашем django.test.TestCase subclass:

from django.test import TestCase
from myapp.models import Animal

class AnimalTestCase(TestCase):
    fixtures = ['mammals.json', 'birds']

    def setUp(self):
        # Test definitions as before.
        call_setup_methods()

    def test_fluffy_animals(self):
        # A test that uses the fixtures.
        call_some_test_code()

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

  • В начале каждого теста, перед выполнением setUp(), Django будет промывать базу данных, возвращая ее в состояние, в котором она находилась непосредственно после вызова migrate.
  • Затем устанавливаются все названные фикстуры. В этом примере Django установит любой JSON фикс с именем mammals, а затем любой фикс с именем birds. Более подробно об определении и установке фикстур смотрите в документации loaddata.

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

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

Конфигурация URLconf

Если ваше приложение предоставляет представления, вы можете включить тесты, которые используют тестовый клиент для выполнения этих представлений. Однако конечный пользователь может свободно развернуть представления в вашем приложении на любом URL по своему выбору. Это означает, что ваши тесты не могут полагаться на то, что ваши представления будут доступны на определенном URL. Украсьте свой тестовый класс или метод теста символом @override_settings(ROOT_URLCONF=...) для конфигурации URLconf.

Поддержка нескольких баз данных

TransactionTestCase.databases

Django устанавливает тестовую базу данных, соответствующую каждой базе данных, которая определена в определении DATABASES в ваших настройках и на которую ссылается хотя бы один тест через databases.

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

В качестве оптимизации, Django промывает только базу данных default в начале каждого запуска теста. Если ваша установка содержит несколько баз данных, и у вас есть тест, который требует очистки каждой базы данных, вы можете использовать атрибут databases в наборе тестов, чтобы запросить очистку дополнительных баз данных.

Например:

class TestMyViews(TransactionTestCase):
    databases = {'default', 'other'}

    def test_index_page_view(self):
        call_some_test_code()

В этом тестовом примере перед выполнением default и other будут промыты тестовые базы данных test_index_page_view. Вы также можете использовать '__all__', чтобы указать, что все тестовые базы данных должны быть промыты.

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

Запросы к базам данных, не входящим в databases, будут выдавать ошибки утверждения, чтобы предотвратить утечку состояния между тестами.

TestCase.databases

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

Используйте атрибут databases class на тестовом классе, чтобы запросить обертывание транзакций против не``default`` баз данных.

Например:

class OtherDBTests(TestCase):
    databases = {'other'}

    def test_other_db_query(self):
        ...

Этот тест разрешит запросы только к базе данных other. Как и для SimpleTestCase.databases и TransactionTestCase.databases, константа '__all__' может быть использована для указания того, что тест должен разрешить запросы ко всем базам данных.

Переопределение настроек

Предупреждение

Используйте приведенные ниже функции для временного изменения значений параметров в тестах. Не манипулируйте django.conf.settings напрямую, так как Django не восстановит исходные значения после таких манипуляций.

SimpleTestCase.settings()[исходный код]

Для целей тестирования часто бывает полезно временно изменить настройки и вернуться к исходному значению после выполнения кода тестирования. Для этого случая Django предоставляет стандартный менеджер контекстов Python (см. PEP 343) под названием settings(), который можно использовать следующим образом:

from django.test import TestCase

class LoginTestCase(TestCase):

    def test_login(self):

        # First check for the default behavior
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/accounts/login/?next=/sekrit/')

        # Then override the LOGIN_URL setting
        with self.settings(LOGIN_URL='/other/login/'):
            response = self.client.get('/sekrit/')
            self.assertRedirects(response, '/other/login/?next=/sekrit/')

Этот пример отменит установку LOGIN_URL для кода в блоке with и после этого сбросит его значение в предыдущее состояние.

SimpleTestCase.modify_settings()[исходный код]

Переопределение параметров, содержащих список значений, может оказаться громоздким. На практике часто бывает достаточно добавить или удалить значения. Django предоставляет контекстный менеджер modify_settings() для более простого изменения настроек:

from django.test import TestCase

class MiddlewareTestCase(TestCase):

    def test_cache_middleware(self):
        with self.modify_settings(MIDDLEWARE={
            'append': 'django.middleware.cache.FetchFromCacheMiddleware',
            'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
            'remove': [
                'django.contrib.sessions.middleware.SessionMiddleware',
                'django.contrib.auth.middleware.AuthenticationMiddleware',
                'django.contrib.messages.middleware.MessageMiddleware',
            ],
        }):
            response = self.client.get('/')
            # ...

Для каждого действия вы можете предоставить либо список значений, либо строку. Если значение уже существует в списке, append и prepend не имеют эффекта; также как и remove, если значение не существует.

override_settings(**kwargs)[исходный код]

Если вы хотите переопределить настройки метода тестирования, Django предоставляет декоратор override_settings() (см. PEP 318). Он используется следующим образом:

from django.test import TestCase, override_settings

class LoginTestCase(TestCase):

    @override_settings(LOGIN_URL='/other/login/')
    def test_login(self):
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/other/login/?next=/sekrit/')

Декоратор также может быть применен к классам TestCase:

from django.test import TestCase, override_settings

@override_settings(LOGIN_URL='/other/login/')
class LoginTestCase(TestCase):

    def test_login(self):
        response = self.client.get('/sekrit/')
        self.assertRedirects(response, '/other/login/?next=/sekrit/')
modify_settings(*args, **kwargs)[исходный код]

Аналогично, Django предоставляет декоратор modify_settings():

from django.test import TestCase, modify_settings

class MiddlewareTestCase(TestCase):

    @modify_settings(MIDDLEWARE={
        'append': 'django.middleware.cache.FetchFromCacheMiddleware',
        'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
    })
    def test_cache_middleware(self):
        response = self.client.get('/')
        # ...

Декоратор также может быть применен к классам тестовых примеров:

from django.test import TestCase, modify_settings

@modify_settings(MIDDLEWARE={
    'append': 'django.middleware.cache.FetchFromCacheMiddleware',
    'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
})
class MiddlewareTestCase(TestCase):

    def test_cache_middleware(self):
        response = self.client.get('/')
        # ...

Примечание

При передаче класса эти декораторы изменяют класс напрямую и возвращают его; они не создают и не возвращают его модифицированную копию. Поэтому если вы попытаетесь изменить приведенные выше примеры, чтобы присвоить возвращаемому значению имя, отличное от LoginTestCase или MiddlewareTestCase, вы можете с удивлением обнаружить, что исходные классы тестовых примеров по-прежнему одинаково подвержены влиянию декоратора. Для данного класса modify_settings() всегда применяется после override_settings().

Предупреждение

Файл настроек содержит некоторые параметры, к которым обращаются только во время инициализации внутренних механизмов Django. Если вы измените их с помощью override_settings, настройка будет изменена, если вы обратитесь к ней через модуль django.conf.settings, однако, внутренние механизмы Django обращаются к ней по-другому. Эффективно, использование override_settings() или modify_settings() с этими настройками, вероятно, не сделает того, чего вы ожидаете.

Мы не рекомендуем изменять настройку DATABASES. Изменение параметра CACHES возможно, но несколько затруднительно, если вы используете внутренние компоненты, использующие кэширование, например django.contrib.sessions. Например, вам придется заново инициализировать бэкенд сессии в тесте, который использует кэшированные сессии и переопределяет CACHES.

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

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

@override_settings()
def test_something(self):
    del settings.LOGIN_URL
    ...

При переопределении настроек убедитесь, что в коде вашего приложения используется кэш или подобная функция, которая сохраняет состояние даже при изменении настроек. Django предоставляет сигнал django.test.signals.setting_changed, который позволяет вам регистрировать обратные вызовы для очистки и другого сброса состояния при изменении настроек.

Сам Django использует этот сигнал для сброса различных данных:

Переопределенные настройки Сброс данных
USE_TZ, TIME_ZONE Часовой пояс баз данных
ТЕМПЛАТЫ Шаблонные двигатели
МОДУЛИ СЕРИАЛИЗАЦИИ Кэш сериализаторов
ЛОКАЛЬНЫЕ_ПУТИ, ЯЗЫКОВОЙ_КОД Перевод по умолчанию и загруженные переводы
MEDIA_ROOT, DEFAULT_FILE_STORAGE Хранилище файлов по умолчанию

Опустошение ящика для анализов

Если вы используете любой из пользовательских классов Django TestCase, программа запуска тестов будет очищать содержимое тестового почтового ящика в начале каждого тестового случая.

Более подробно об услугах электронной почты во время тестирования см. ниже Email services.

Утверждения

Поскольку обычный класс Python unittest.TestCase реализует такие методы утверждения, как assertTrue() и assertEqual(), пользовательский класс Django TestCase предоставляет ряд пользовательских методов утверждения, которые полезны для тестирования веб-приложений:

Сообщения о сбоях, выдаваемые большинством этих методов утверждения, можно настроить с помощью аргумента msg_prefix. Эта строка будет добавлена к любому сообщению о сбое, сгенерированному утверждением. Это позволяет вам предоставить дополнительные подробности, которые могут помочь вам определить место и причину сбоя в вашем тестовом наборе.

SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs)[исходный код]
SimpleTestCase.assertRaisesMessage(expected_exception, expected_message)

Утверждает, что выполнение callable вызывает expected_exception и что expected_message находится в сообщении исключения. При любом другом исходе сообщается о неудаче. Это более простая версия unittest.TestCase.assertRaisesRegex() с той разницей, что expected_message не рассматривается как регулярное выражение.

Если заданы только параметры expected_exception и expected_message, возвращает менеджер контекста, так что тестируемый код может быть написан inline, а не как функция:

with self.assertRaisesMessage(ValueError, 'invalid literal for int()'):
    int('a')
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message, callable, *args, **kwargs)[исходный код]
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message)

Аналогично SimpleTestCase.assertRaisesMessage(), но для assertWarnsRegex() вместо assertRaisesRegex().

SimpleTestCase.assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value='')[исходный код]

Утверждает, что поле формы ведет себя правильно при различных вводах.

Параметры:
  • fieldclass – класс тестируемого поля.
  • valid – словарь, отображающий действительные входные данные на их ожидаемые очищенные значения.
  • invalid – словарь, отображающий недопустимые входные данные на одно или несколько сообщений об ошибках.
  • field_args – args, переданные для инстанцирования поля.
  • field_kwargs – kwargs, переданные для инстанцирования поля.
  • empty_value – ожидаемый чистый выход для входов в empty_values.

Например, следующий код проверяет, что EmailField принимает a@a.com как действительный адрес электронной почты, но отвергает aaa с разумным сообщением об ошибке:

self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': ['Enter a valid email address.']})
SimpleTestCase.assertFormError(response, form, field, errors, msg_prefix='')[исходный код]

Утверждает, что поле на форме вызывает указанный список ошибок при отображении на форме.

response должен быть экземпляром ответа, возвращенным test client.

form is the name the Form instance was given in the template context of the response.

field - это имя поля на форме для проверки. Если field имеет значение None, будут проверяться ошибки, не относящиеся к полю (ошибки, к которым можно получить доступ через form.non_field_errors()).

errors - это строка ошибки или список строк ошибок, которые ожидаются в результате валидации формы.

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

Утверждает, что formset при отображении вызывает указанный список ошибок.

response должен быть экземпляром ответа, возвращенным test client.

formset is the name the Formset instance was given in the template context of the response.

form_index - это номер формы внутри Formset. Если form_index имеет значение None, будут проверяться ошибки, не относящиеся к форме (ошибки, к которым можно получить доступ через formset.non_form_errors()).

field - это имя поля на форме для проверки. Если field имеет значение None, будут проверяться ошибки, не относящиеся к полю (ошибки, к которым можно получить доступ через form.non_field_errors()).

errors - это строка ошибки или список строк ошибок, которые ожидаются в результате валидации формы.

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 не имеет значения.
  • Булевы атрибуты (например, checked) без аргумента равны атрибутам, равным по имени и значению (см. примеры).
  • Текст, ссылки на символы и ссылки на сущности, которые ссылаются на один и тот же символ, эквивалентны.

Следующие примеры являются корректными тестами и не вызывают никаких AssertionError:

self.assertHTMLEqual(
    '<p>Hello <b>&#x27;world&#x27;!</p>',
    '''<p>
        Hello   <b>&#39;world&#39;! </b>
    </p>'''
)
self.assertHTMLEqual(
    '<input type="checkbox" checked="checked" id="id_accept_terms" />',
    '<input id="id_accept_terms" type="checkbox" checked>'
)

html1 и html2 должны содержать HTML. Если одно из них не может быть разобрано, будет выдано сообщение AssertionError.

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

Changed in Django 4.0:

В старых версиях любой атрибут (не только булевы атрибуты) без значения считался равным атрибуту с тем же именем и значением.

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

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

html1 и html2 должны содержать HTML. Если одно из них не может быть разобрано, будет выдано сообщение AssertionError.

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

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

Утверждает, что строки xml1 и xml2 равны. Сравнение основано на семантике XML. Аналогично assertHTMLEqual(), сравнение производится по разобранному содержимому, поэтому учитываются только семантические различия, а не синтаксические. Если в любом параметре передан недопустимый XML, всегда выдается предупреждение AssertionError, даже если обе строки идентичны.

Объявление XML, тип документа, инструкции по обработке и комментарии игнорируются. Сравниваются только корневой элемент и его дочерние элементы.

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

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

Утверждает, что строки xml1 и xml2 не равны. Сравнение основано на семантике XML. Подробности см. в assertXMLEqual().

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

SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix='')[исходный код]

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

Если указан целочисленный аргумент count, то дополнительно будет строго проверяться количество вхождений needle.

Пробельные символы в большинстве случаев игнорируются, а порядок следования атрибутов не имеет значения. См. раздел assertHTMLEqual() для более подробной информации.

SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None)[исходный код]

Утверждает, что фрагменты JSON raw и expected_data равны. Применяются обычные правила JSON о несущественных пробельных символах, так как тяжелый груз передается библиотеке json.

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

SimpleTestCase.assertJSONNotEqual(raw, expected_data, msg=None)[исходный код]

Утверждает, что фрагменты JSON raw и expected_data не равны. См. assertJSONEqual() для более подробной информации.

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

TransactionTestCase.assertQuerysetEqual(qs, values, transform=None, ordered=True, msg=None)[исходный код]

Утверждает, что кверисет qs соответствует определенному итеративу значений values.

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

По умолчанию сравнение также зависит от порядка. Если qs не обеспечивает неявного упорядочивания, вы можете установить параметр ordered в значение False, что превратит сравнение в сравнение collections.Counter. Если порядок не определен (если данное qs не упорядочено и сравнение производится с более чем одним упорядоченным значением), возникает ошибка ValueError.

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

Changed in Django 3.2:

Значение по умолчанию аргумента transform было изменено на None.

New in Django 3.2:

Добавлена поддержка прямого сравнения между наборами запросов.

Не рекомендуется, начиная с версии 3.2: Если transform не указан и values является списком строк, он сравнивается со списком, полученным путем применения repr() к каждому члену qs. Это поведение устарело и будет удалено в Django 4.1. Если вам это нужно, явно установите transform в repr.

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 в любом тесте.

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

  • Параметр follow не поддерживается.

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

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

При использовании AsyncClient любой метод, выполняющий запрос, должен быть ожидаемым:

async def test_my_thing(self):
    response = await self.async_client.get('/some-url/')
    self.assertEqual(response.status_code, 200)

Асинхронный клиент может также вызывать синхронные представления; он запускается через asynchronous request path Django, который поддерживает оба варианта. Любое представление, вызванное через AsyncClient, получит объект ASGIRequest для своего request, а не WSGIRequest, который создает обычный клиент.

Предупреждение

Если вы используете тестовые декораторы, они должны быть async-совместимыми, чтобы гарантировать их корректную работу. Встроенные в Django декораторы будут вести себя правильно, но сторонние декораторы могут оказаться невыполненными (они «обернут» не ту часть потока выполнения, а не ваш тест).

Если вам необходимо использовать эти декораторы, то вместо них украсьте свои тестовые методы с помощью async_to_sync() внутри них:

from asgiref.sync import async_to_sync
from django.test import TestCase

class MyTests(TestCase):

    @mock.patch(...)
    @async_to_sync
    async def test_my_thing(self):
        ...

Услуги электронной почты

Если какое-либо из ваших представлений Django отправляет электронную почту, используя Django’s email functionality, вы, вероятно, не хотите отправлять электронную почту каждый раз, когда запускаете тест, использующий это представление. По этой причине бегунок тестирования Django автоматически перенаправляет все отправленные Django письма в фиктивный почтовый ящик. Это позволяет вам тестировать все аспекты отправки электронной почты - от количества отправленных сообщений до содержимого каждого сообщения - без фактической отправки сообщений.

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

django.core.mail.outbox

Во время тестирования каждое исходящее письмо сохраняется в django.core.mail.outbox. Это список всех экземпляров EmailMessage, которые были отправлены. Атрибут outbox - это специальный атрибут, который создается только при использовании бэкенда электронной почты locmem. Обычно он не существует как часть модуля django.core.mail, и вы не можете импортировать его напрямую. В приведенном ниже коде показано, как правильно обращаться к этому атрибуту.

Вот пример теста, который проверяет django.core.mail.outbox на длину и содержимое:

from django.core import mail
from django.test import TestCase

class EmailTest(TestCase):
    def test_send_email(self):
        # Send message.
        mail.send_mail(
            'Subject here', 'Here is the message.',
            'from@example.com', ['to@example.com'],
            fail_silently=False,
        )

        # Test that one message has been sent.
        self.assertEqual(len(mail.outbox), 1)

        # Verify that the subject of the first message is correct.
        self.assertEqual(mail.outbox[0].subject, 'Subject here')

Как было отмечено previously, тестовый outbox опустошается в начале каждого теста в Django *TestCase. Чтобы опустошить папку outbox вручную, назначьте пустой список на mail.outbox:

from django.core import mail

# Empty the test outbox
mail.outbox = []

Команды управления

Команды управления могут быть проверены с помощью функции call_command(). Вывод может быть перенаправлен в экземпляр StringIO:

from io import StringIO
from django.core.management import call_command
from django.test import TestCase

class ClosepollTest(TestCase):
    def test_command_output(self):
        out = StringIO()
        call_command('closepoll', stdout=out)
        self.assertIn('Expected output', out.getvalue())

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

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

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

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

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

skipIfDBFeature(*feature_name_strings)[исходный код]

Пропустите тест на декорирование или TestCase, если все названные функции базы данных поддерживаются.

Например, следующий тест не будет выполнен, если база данных поддерживает транзакции (например, он не будет выполняться в PostgreSQL, но будет выполняться в MySQL с таблицами MyISAM):

class MyTests(TestCase):
    @skipIfDBFeature('supports_transactions')
    def test_transaction_behavior(self):
        # ... conditional test code
        pass
skipUnlessDBFeature(*feature_name_strings)[исходный код]

Пропустите тест на декорирование или TestCase, если какая-либо из названных функций базы данных не поддерживается.

Например, следующий тест будет выполнен, только если база данных поддерживает транзакции (например, он будет выполняться в PostgreSQL, но не в MySQL с таблицами MyISAM):

class MyTests(TestCase):
    @skipUnlessDBFeature('supports_transactions')
    def test_transaction_behavior(self):
        # ... conditional test code
        pass
Вернуться на верх