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

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

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

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

Некоторые вещи, которые вы можете делать с тестовым клиентом:

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

Обратите внимание, этот клиент не может заменить Selenium или другие фреймворки, которые используют движок браузера для запросов. У тестового клиента 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.

Несколько заметок о работе тестового клиента:

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

The json_encoder argument allows setting a custom JSON encoder for the JSON serialization that’s described in post().

The raise_request_exception argument allows controlling whether or not exceptions raised during the request should also be raised in the test. Defaults to True.

New in Django 3.0:

The raise_request_exception argument was added.

Создав экземпляр 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 позволяют указать дополнительные HTTP заголовки. Например:

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

…отправит HTTP заголовок HTTP_X_REQUESTED_WITH, который позволит протестировать обработку django.http.HttpRequest.is_ajax().

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

Заголовки из **extra должны соответствовать CGI спецификации. Например, различные значения для HTTP заголовка «Host» следует указать в параметре 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-ов и статусов ответа.

Если у вас есть 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

If you provide content_type as application/json, the data is serialized using json.dumps() if it’s a dict, list, or tuple. Serialization is performed with DjangoJSONEncoder by default, and can be overridden by providing a json_encoder argument to Client. This serialization also happens for put(), patch(), and delete() requests.

Changed in Django 2.2:

The JSON serialization was extended to support lists and tuples. In older versions, only dicts are serialized.

If you provide any other content_type (e.g. text/xml for an XML payload), the contents of data are sent as-is in the POST request, using content_type in the HTTP Content-Type header.

Если content_type не указан, данные из data будут отправлены с типом multipart/form-data. В этом случае значения из data будут кодированы как «multipart» сообщение и отправлены через POST.

Чтобы отправить несколько значений для одного ключа – например, указать список значений из <select multiple> – укажите список или кортеж значений. Например, следующий data отправит три значения для поля с названием choices:

{'choices': ('a', 'b', 'd')}

Отправка файлов – особый случай. Чтобы отправить файлы через POST запрос, укажите название файлового поля в качестве ключа и открытый файл в качестве значения. Например:

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

(Название attachment взято в качестве примера, используйте название из вашего кода.)

You may also provide any file-like object (e.g., StringIO or BytesIO) as a file handle. If you’re uploading to an ImageField, the object needs a name attribute that passes the validate_image_file_extension validator. For example:

>>> 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 будет содержать кортеж из всех URL-ов и статусов ответа.

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

head(path, data=None, follow=False, secure=False, **extra)

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

options(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)

Выполняет OPTIONS запрос по указанному path и возвращает объект ``Response``(описан ниже). Удобен при тестировании REST API.

Если передать 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``(описан ниже). Удобен при тестировании REST API.

Если передать 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``(описан ниже). Удобен при тестировании REST API.

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

delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)

Makes a DELETE request on the provided path and returns a Response object. Useful for testing RESTful interfaces.

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

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

trace(path, follow=False, secure=False, **extra)

Выполняет TRACE запрос по указанному path и возвращает объект Response. Удобен при тестировании.

Unlike the other request methods, data is not provided as a keyword parameter in order to comply with RFC 7231#section-4.3.8, which mandates that TRACE requests must not have a body.

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

login(**credentials)

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

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

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

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

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

Для других бэкендов авторизации параметры могут отличаться. Это зависит от аргументов, которые принимает метод authenticate() бэкенда авторизации.

login() возвращает True, если пользователь успешно залогинен.

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

force_login(user, backend=None)

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

В отличии от login() этот метод пропускает этапы авторизации и проверки пользователя: неактивные пользователи (is_active=False) могут быть авторизированы и нет надобности указывать данные для авторизации(прим. пер. логин/пароль).

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

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

logout()

Если ваш сайт использует систему авторизации Django, вы можете использовать метод logout() чтобы эмулировать логаут пользователя.

После вызова метода тестовый клиент очистит все куки и сессионные данные. Последующие запросы будт выполнены от 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
New in Django 3.0.

A tuple of three values that provides information about the unhandled exception, if any, that occurred during the view.

The values are (type, value, traceback), the same as returned by Python’s sys.exc_info(). Their meanings are:

  • type: The type of the exception.
  • value: The exception instance.
  • traceback: A traceback object which encapsulates the call stack at the point where the exception originally occurred.

If no exception occurred, then exc_info will be 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

The HTTP status of the response, as an integer. For a full list of defined codes, see the IANA status code registry.

templates

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

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

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

resolver_match

An instance of ResolverMatch for the response. You can use the func attribute, for example, to verify the view that served the response:

# 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__)

If the given URL is not found, accessing this attribute will raise a Resolver404 exception.

Вы можете использовать синтаксис словаря, чтобы получить значение HTTP заголовков. Например, можно получить тип ответа, используя response['Content-Type'].

Исключения

If you point the test client at a view that raises an exception and Client.raise_request_exception is True, that exception will be visible in the test case. You can then use a standard try ... except block or assertRaises() to test for exceptions.

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

If Client.raise_request_exception is False, the test client will return a 500 response as would be returned to a browser. The response has the attribute exc_info to provide information about the unhandled exception.

Сохранение состояния

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

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

Клиент хранит состояние в двух атрибутах. Вы можете использовать их в тестах.

Client.cookies

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

Client.session

Объект с API словаря, который содержит данные сессии. Подробности смотрите в в разделе о сессии.

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

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

Setting the language

When testing applications that support internationalization and localization, you might want to set the language for a test client request. The method for doing so depends on whether or not the LocaleMiddleware is enabled.

If the middleware is enabled, the language can be set by creating a cookie with a name of LANGUAGE_COOKIE_NAME and a value of the language code:

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.")

or by including the Accept-Language HTTP header in the request:

def test_language_using_header(self):
    response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='fr')
    self.assertEqual(response.content, b"Bienvenue sur mon site.")

More details are in Как Django определяет языковую настройку.

If the middleware isn’t enabled, the active language may be set using 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.")

More details are in Явное указание активного языка.

Пример

The following is a unit test using the test client:

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

You can convert a normal unittest.TestCase to any of the subclasses: change the base class of your test from unittest.TestCase to the subclass. All of the standard Python unit test functionality will be available, and it will be augmented with some useful additions as described in each section below.

SimpleTestCase

class SimpleTestCase

A subclass of unittest.TestCase that adds this functionality:

If your tests make any database queries, use subclasses TransactionTestCase or TestCase.

SimpleTestCase.databases
New in Django 2.2.

SimpleTestCase disallows database queries by default. This helps to avoid executing write queries which will affect other tests since each SimpleTestCase test isn’t run in a transaction. If you aren’t concerned about this problem, you can disable this behavior by setting the databases class attribute to '__all__' on your test class.

SimpleTestCase.allow_database_queries

Не рекомендуется, начиная с версии 2.2.

This attribute is deprecated in favor of databases. The previous behavior of allow_database_queries = True can be achieved by setting databases = '__all__'.

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

SimpleTestCase и его потомки (т.е., TestCase, …) используют методы setUpClass() и tearDownClass() для выполнения некоторой инициализации в рамках класса (т.е., для переопределения настроек). Если вам понадобится переопределить эти методы, не забудьте воспользоваться оператором super:

class MyTestCase(TestCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        ...

    @classmethod
    def tearDownClass(cls):
        ...
        super().tearDownClass()

Be sure to account for Python’s behavior if an exception is raised during setUpClass(). If that happens, neither the tests in the class nor tearDownClass() are run. In the case of django.test.TestCase, this will leak the transaction created in super() which results in various symptoms including a segmentation fault on some platforms (reported on macOS). If you want to intentionally raise an exception such as unittest.SkipTest in setUpClass(), be sure to do it before calling super() to avoid this.

TransactionTestCase

class TransactionTestCase

TransactionTestCase inherits from SimpleTestCase to add some database-specific features:

Django’s TestCase class is a more commonly used subclass of TransactionTestCase that makes use of database transaction facilities to speed up the process of resetting the database to a known state at the beginning of each test. A consequence of this, however, is that some database behaviors cannot be tested within a Django TestCase class. For instance, you cannot test that a block of code is executing within a transaction, as is required when using select_for_update(). In those cases, you should use TransactionTestCase.

TransactionTestCase и TestCase отличаются лишь механизмом сброса базы данных перед каждым тестом и мозможностью тестировать транзакции:

  • TransactionTestCase сбрасывает состояние базы данных после выполнения тестам путем очистки всех таблиц. TransactionTestCase позволяет вызывать коммит и отмену транзакций и проверять результат в базе данных.
  • TestCase, в свою очередь, не очищает все таблицы в конце теста. Вместо этого, каждый тест оборачивается в транзакцию, которые откатывается по завершению теста. Это гарантирует, что откат в конце теста вернёт базу данных в начальное состояние.

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

TestCase, который выполняется на базе данных, которая не поддерживает транзакции (например, MySQL с MyISAM), и все TransactionTestCase, после завершения теста очищают таблицы.

Данные приложения не будут перезагружены; если это необходимо (например, распространяемые приложения должны использовать это) вы можете указать serialized_rollback = True для класса TestCase.

TestCase

class TestCase

This is the most common class to use for writing tests in Django. It inherits from TransactionTestCase (and by extension SimpleTestCase). If your Django application doesn’t use a database, use SimpleTestCase.

The class:

  • Wraps the tests within two nested atomic() blocks: one for the whole class and one for each test. Therefore, if you want to test some specific database transaction behavior, use TransactionTestCase.
  • Checks deferrable database constraints at the end of each test.

It also provides an additional method:

classmethod TestCase.setUpTestData()

Описанный выше atomic блок уровня класса позволяет создание начальных данных на уровне класса, один раз для всего TestCase. Такой подход пригодится для быстрых тестов, по сравнению с обычным использованием метода setUp().

Например:

from django.test import TestCase

class MyTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Set up data for the whole TestCase
        cls.foo = Foo.objects.create(bar="Test")
        ...

    def test1(self):
        # Some test using self.foo
        ...

    def test2(self):
        # Some other test using self.foo
        ...

Следует отметить, что если тесты запущены на базе данных, у которой нет поддержки транзакций (например, MySQL с движком MyISAM), то метод setUpTestData() будет вызван перед выполнением каждого теста, ухудшая показатели скорости.

Будьте осторожны, не меняйте объекты, которые были созданы в setUpTestData(), в тестовых метод. Изменение этих объектов, которые хранятся в памяти, будет влиять на все тесты текущего класса. Если вам все же необходимо их изменить, вы можете перезагрузить их в методе setUp(), используя, например, refresh_from_db().

LiveServerTestCase

class LiveServerTestCase

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

The live server listens on localhost and binds to port 0 which uses a free port assigned by the operating system. The server’s URL can be accessed with self.live_server_url during the tests.

To demonstrate how to use LiveServerTestCase, let’s write a Selenium test. First of all, you need to install the selenium package into your Python path:

$ 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, затем страницу авторизации, вводит логин и пароль и нажимает кнопку «Log in». Selenium поддерживает и другие драйверы, если у вас не установлен Firefox, или вы хотите использовать другие браузеры. Пример выше показывает лишь малую часть возможностей Selenium; подробности смотрите в документации.

Примечание

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

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

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. Этот клиент пересоздается для каждого теста, поэтому вам не нужно беспокоится о состоянии (например о куках) между тестами.

Поэтому вместо создания экземпляра Client в каждом тесте:

import unittest
from django.test import Client

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

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

…you can refer to self.client, like so:

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.

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

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

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

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

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

Что произойдет:

  • At the start of each test, before setUp() is run, Django will flush the database, returning the database to the state it was in directly after migrate was called.
  • Затем загружаются фикстуры. В примере выше Django загрузит JSON фикстуру с названием mammals, затем фикстуру birds. Подробности смотрите в описании loaddata.

For performance reasons, TestCase loads fixtures once for the entire test class, before setUpTestData(), instead of before each test, and it uses transactions to clean the database before each test. In any case, you can be certain that the outcome of a test will not be affected by another test or by the order of test execution.

By default, fixtures are only loaded into the default database. If you are using multiple databases and set TransactionTestCase.databases, fixtures will be loaded into all specified databases.

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

If your application provides views, you may want to include tests that use the test client to exercise those views. However, an end user is free to deploy the views in your application at any URL of their choosing. This means that your tests can’t rely upon the fact that your views will be available at a particular URL. Decorate your test class or test method with @override_settings(ROOT_URLCONF=...) for URLconf configuration.

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

TransactionTestCase.databases
New in Django 2.2.

Django sets up a test database corresponding to every database that is defined in the DATABASES definition in your settings and referred to by at least one test through databases.

However, a big part of the time taken to run a Django TestCase is consumed by the call to flush that ensures that you have a clean database at the start of each test run. If you have multiple databases, multiple flushes are required (one for each database), which can be a time consuming activity – especially if your tests don’t need to test multi-database activity.

As an optimization, Django only flushes the default database at the start of each test run. If your setup contains multiple databases, and you have a test that requires every database to be clean, you can use the databases attribute on the test suite to request extra databases to be flushed.

Например:

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

    def test_index_page_view(self):
        call_some_test_code()

This test case will flush the default and other test databases before running test_index_page_view. You can also use '__all__' to specify that all of the test databases must be flushed.

The databases flag also controls which databases the TransactionTestCase.fixtures are loaded into. By default, fixtures are only loaded into the default database.

Queries against databases not in databases will give assertion errors to prevent state leaking between tests.

TransactionTestCase.multi_db

Не рекомендуется, начиная с версии 2.2.

This attribute is deprecated in favor of databases. The previous behavior of multi_db = True can be achieved by setting databases = '__all__'.

TestCase.databases
New in Django 2.2.

By default, only the default database will be wrapped in a transaction during a TestCase’s execution and attempts to query other databases will result in assertion errors to prevent state leaking between tests.

Use the databases class attribute on the test class to request transaction wrapping against non-default databases.

Например:

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

    def test_other_db_query(self):
        ...

This test will only allow queries against the other database. Just like for SimpleTestCase.databases and TransactionTestCase.databases, the '__all__' constant can be used to specify that the test should allow queries to all databases.

TestCase.multi_db

Не рекомендуется, начиная с версии 2.2.

This attribute is deprecated in favor of databases. The previous behavior of multi_db = True can be achieved by setting 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()

It can prove unwieldy to redefine settings that contain a list of values. In practice, adding or removing values is often sufficient. Django provides the modify_settings() context manager for easier settings changes:

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()

Если вы хотите переопределить настройки для тестового метода, 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()

Также 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 илиr MiddlewareTestCase, вы обнаружите, что оригинальные классы были все равно изменены декораторами. Декоратор modify_settings() всегда применяет после override_settings(), если их добавить к одному классу.

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

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

Мы не советуем изменять настройку DATABASES. Менять CACHES можно, но требует дополнительных действий, если кеш используется другими механизмами Django, например django.contrib.sessions. В таком случае вам понадобиться заново инициализировать сессию в тесте после изменения CACHES.

Также не указывайте ваши настройки в константах модуля, override_settings() не будет работать с такими настройками т.к. они выполняются только при первом импорте модуля. (FIXME: whut?)

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

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

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

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

Переопределенные настройки Данные, который сбрасываются
USE_TZ, TIME_ZONE Часовой пояс баз данных
TEMPLATES Шаблонные движки
SERIALIZATION_MODULES Кеш сериализаторов
LOCALE_PATHS, LANGUAGE_CODE Перевод по умолчанию и загруженные переводы
MEDIA_ROOT, DEFAULT_FILE_STORAGE Хранилище файлов по умолчанию

Очистка тестовых электронных писем

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

Подробности смотрите ниже в разделе об отправке писем.

Проверки

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

The failure messages given by most of these assertion methods can be customized with the msg_prefix argument. This string will be prefixed to any failure message generated by the assertion. This allows you to provide additional details that may help you to identify the location and cause of a failure in your test suite.

SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs)
SimpleTestCase.assertRaisesMessage(expected_exception, expected_message)

Asserts that execution of callable raises expected_exception and that expected_message is found in the exception’s message. Any other outcome is reported as a failure. It’s a simpler version of unittest.TestCase.assertRaisesRegex() with the difference that expected_message isn’t treated as a regular expression.

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

with self.assertRaisesMessage(ValueError, 'invalid literal for int()'):
    int('a')
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message, callable, *args, **kwargs)
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message)

Analogous to SimpleTestCase.assertRaisesMessage() but for assertWarnsRegex() instead of assertRaisesRegex().

SimpleTestCase.assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value='')

Проверяет поведение поля формы с разными значениями.

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

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

self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': ['Enter a valid email address.']})
SimpleTestCase.assertFormError(response, form, field, errors, msg_prefix='')

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

form – название экземпляра Form в контексте шаблона ответа.

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

errors – строка с ошибкой, или список ошибок, которые были получены при валидации формы.

SimpleTestCase.assertFormsetError(response, formset, form_index, field, errors, msg_prefix='')

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

formset – название экземпляра Formset в контексте шаблона ответа.

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)

Проверяет, что объект Response с указанным status_code и содержит text. Если указан count, text должен встречаться count раз в ответе.

Укажите True в html, чтобы text обрабатывался как HTML. Сравнение содержимого будет основано на семантике HTML, а не посимвольном сравнении. Пробелы будут проигнорированы в большинстве случаев, порядок атрибутов не учитывается. Подробности с мотрите в описании assertHTMLEqual().

SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False)

Проверяет, что объект Response с указанным status_code и не содержит text.

Укажите True в html, чтобы text обрабатывался как HTML. Сравнение содержимого будет основано на семантике HTML, а не посимвольном сравнении. Пробелы будут проигнорированы в большинстве случаев, порядок атрибутов не учитывается. Подробности с мотрите в описании assertHTMLEqual().

SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix='', count=None)

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

Название – строка, например 'admin/index.html'.

Количество аргументов - это целое, показывающее количество раз, которое будет обработан шаблон. По умолчанию это None, означающее, что шаблон должен быть обработан один или борее раз.

Можно использовать как менеджер контекста:

with self.assertTemplateUsed('index.html'):
    render_to_string('index.html')
with self.assertTemplateUsed(template_name='index.html'):
    render_to_string('index.html')
SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix='')

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

Можно использовать как менеджер контекста, как и assertTemplateUsed().

SimpleTestCase.assertURLEqual(url1, url2, msg_prefix='')
New in Django 2.2.

Asserts that two URLs are the same, ignoring the order of query string parameters except for parameters with the same name. For example, /path/?x=1&y=2 is equal to /path/?y=2&x=1, but /path/?a=1&a=2 isn’t equal to /path/?a=2&a=1.

SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='', fetch_redirect_response=True)

Проверяет, что status_code ответа указывает на редирект на expected_url (включая GET параметры), и финальная страница загружается со статусом target_status_code.

Если запрос использует аргумент follow, expected_url и target_status_code будут указывать на последний запрос.

If fetch_redirect_response is False, the final page won’t be loaded. Since the test client can’t fetch external URLs, this is particularly useful if expected_url isn’t part of your Django app.

Протокол правильно обрабатывается при сравнении URL-ов. Если протокол не указан при редиректе, будет использоваться протокол изначального запроса.

SimpleTestCase.assertHTMLEqual(html1, html2, msg=None)

Сравнивает строки html1 и html2. Сравнение основано на семантике HTML. При сравнении используются следующие правила:

  • Пробелы перед и после HTML тегов игнорируются.
  • Все типы пробелов считаются одинаковыми.
  • Все незакрытые теги закрываются, например, при закрытии внешнего тега, или в конце HTML документа.
  • Пустые теги равны самозакрывающиемся аналогичным тегам.
  • Порядок атрибутов HTML тегов не учитывается.
  • Атрибуты без значений равны атрибутам, значение которых равно названию атрибута (смотрите примеры).
  • Text, character references, and entity references that refer to the same character are equivalent.

Следующие тесты не вызывают 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.

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 declaration, document type, and comments are ignored. Only the root element and its children are compared.

Вы можете изменить сообщение об ошибке с помощью аргумента 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 встречается указанное число раз.

В большинстве случаев пробелы игнорируются. Аргументы должны содержать правильный HTML.

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=repr, ordered=True, msg=None)

Проверяет, что qs``(объект ``QuerySet) возвращает список значений values.

При сравнении содержимого qs и values используется функция transform; по умолчанию сравнивается результат repr() над каждым значением. Вы можете указать другую функцию, если repr() возвращает не уникальные значения, которые нельзя использовать при сравнении.

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

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

TransactionTestCase.assertNumQueries(num, func, *args, **kwargs)

Проверяет, что при вызове функции func с аргументами *args и **kwargs выполнилось num запросов в базу данных.

If a "using" key is present in kwargs it is used as the database alias for which to check the number of queries:

self.assertNumQueries(7, using='non_default_db')

If you wish to call a function with a using parameter you can do it by wrapping the call with a lambda to add an extra parameter:

self.assertNumQueries(7, lambda: my_function(using=7))

Вы можете использовать этот метод как менеджер контекста:

with self.assertNumQueries(2):
    Person.objects.create(name="Aaron")
    Person.objects.create(name="Daniel")

Tagging tests

You can tag your tests so you can easily run a particular subset. For example, you might label fast or slow tests:

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):
        ...

You can also tag a test case:

@tag('slow', 'core')
class SampleTestCase(TestCase):
    ...

Subclasses inherit tags from superclasses, and methods inherit tags from their class. Given:

@tag('foo')
class SampleTestCaseChild(SampleTestCase):

    @tag('bar')
    def test(self):
        ...

SampleTestCaseChild.test will be labeled with 'slow', 'core', 'bar', and 'foo'.

Then you can choose which tests to run. For example, to run only fast tests:

$ ./manage.py test --tag=fast
...\> manage.py test --tag=fast

Or to run fast tests and the core one (even though it’s slow):

$ ./manage.py test --tag=fast --tag=core
...\> manage.py test --tag=fast --tag=core

You can also exclude tests by tag. To run core tests if they are not slow:

$ ./manage.py test --tag=core --exclude-tag=slow
...\> manage.py test --tag=core --exclude-tag=slow

test --exclude-tag has precedence over test --tag, so if a test has two tags and you select one of them and exclude the other, the test won’t be run.

Сервисы для отправки писем

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

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

django.core.mail.outbox

During test running, each outgoing email is saved in django.core.mail.outbox. This is a list of all EmailMessage instances that have been sent. The outbox attribute is a special attribute that is created only when the locmem email backend is used. It doesn’t normally exist as part of the django.core.mail module and you can’t import it directly. The code below shows how to access this attribute correctly.

Этот пример проверяет размер и содержимое 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')

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

from django.core import mail

# Empty the test outbox
mail.outbox = []

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

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

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

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

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

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

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

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

Декораторы используют строковые называние возможностей базы данных. Строка соответствует атрибуту из класса django.db.backends.BaseDatabaseFeatures, смотрите описание класса, чтобы узнать полный список возможностей базы данных, которые можно проверять.

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