Расширенные темы тестирования¶
Фабрика RequestFactory использует тот же API, что и тестовый клиент. Однако вместо того, чтобы вести себя как браузер, RequestFactory предоставляет способ создания экземпляра запроса, который может быть использован в качестве первого аргумента любого представления. Это означает, что вы можете тестировать функцию представления так же, как и любую другую функцию — как черный ящик, с точно известными входами, тестируя определенные выходы.
API для RequestFactory является слегка ограниченным подмножеством API тестового клиента:
- Он имеет доступ только к HTTP методам get() , post() , put() , delete() , head() , options() и trace() .
- Эти методы принимают все те же аргументы, за исключением follow . Поскольку это просто фабрика для создания запросов, обработка ответа зависит от вас.
- Оно не поддерживает промежуточное программное обеспечение. Атрибуты сессии и аутентификации должны быть предоставлены самим тестом, если это необходимо для правильной работы представления.
Changed in Django 4.2:
Добавлен параметр headers .
Пример¶
Ниже приведен модульный тест, использующий фабрику запросов:
from django.contrib.auth.models import AnonymousUser, User from django.test import RequestFactory, TestCase from .views import MyView, my_view class SimpleTest(TestCase): def setUp(self): # Every test needs access to the request factory. self.factory = RequestFactory() self.user = User.objects.create_user( username="jacob", email="jacob@…", password="top_secret" ) def test_details(self): # Create an instance of a GET request. request = self.factory.get("/customer/details") # Recall that middleware are not supported. You can simulate a # logged-in user by setting request.user manually. request.user = self.user # Or you can simulate an anonymous user by setting request.user to # an AnonymousUser instance. request.user = AnonymousUser() # Test my_view() as if it were deployed at /customer/details response = my_view(request) # Use this syntax for class-based views. response = MyView.as_view()(request) self.assertEqual(response.status_code, 200)
AsyncRequestFactory¶
class AsyncRequestFactory [исходный код] ¶
RequestFactory создает WSGI-подобные запросы. Если вы хотите создавать ASGI-подобные запросы, включая наличие корректного ASGI scope , вы можете вместо этого использовать django.test.AsyncRequestFactory .
Этот класс напрямую API-совместим с RequestFactory , с той лишь разницей, что он возвращает экземпляры ASGIRequest , а не WSGIRequest . Все его методы по-прежнему являются синхронными callables.
Произвольные аргументы ключевых слов в defaults добавляются непосредственно в область видимости ASGI.
Changed in Django 4.2:
Добавлен параметр headers .
Тестирование представлений на основе классов¶
Для тестирования представлений на основе классов вне цикла запрос/ответ вы должны убедиться, что они настроены правильно, вызвав setup() после инстанцирования.
Например, предположим следующее представление на основе классов:
from django.views.generic import TemplateView class HomeView(TemplateView): template_name = "myapp/home.html" def get_context_data(self, **kwargs): kwargs["environment"] = "Production" return super().get_context_data(**kwargs)
Вы можете напрямую протестировать метод get_context_data() , сначала инстанцировав представление, затем передав request в setup() , прежде чем перейти к коду вашего теста:
from django.test import RequestFactory, TestCase from .views import HomeView class HomePageTest(TestCase): def test_environment_set_in_context(self): request = RequestFactory().get("/") view = HomeView() view.setup(request) context = view.get_context_data() self.assertIn("environment", context)
Тесты и несколько имен хостов¶
Настройка ALLOWED_HOSTS проверяется при запуске тестов. Это позволяет тестовому клиенту различать внутренние и внешние URL.
Проекты, которые поддерживают многопользовательскую аренду или иным образом изменяют бизнес-логику на основе хоста запроса и используют пользовательские имена хостов в тестах, должны включать эти хосты в ALLOWED_HOSTS .
Первый способ сделать это — добавить хосты в файл настроек. Например, набор тестов для docs.djangoproject.com включает следующее:
from django.test import TestCase class SearchFormTestCase(TestCase): def test_empty_get(self): response = self.client.get( "/en/dev/search/", headers="host": "docs.djangoproject.dev:8000">, ) self.assertEqual(response.status_code, 200)
и файл настроек включает список доменов, поддерживаемых проектом:
ALLOWED_HOSTS = ["www.djangoproject.dev", "docs.djangoproject.dev", . ]
Другой вариант — добавить необходимые хосты к ALLOWED_HOSTS , используя override_settings() или modify_settings() . Этот вариант может быть предпочтительным в автономных приложениях, которые не могут упаковать свой собственный файл настроек, или в проектах, где список доменов не статичен (например, поддомены для мультитенансинга). Например, вы можете написать тест для домена http://otherserver/ следующим образом:
from django.test import TestCase, override_settings class MultiDomainTestCase(TestCase): @override_settings(ALLOWED_HOSTS=["otherserver"]) def test_other_domain(self): response = self.client.get("http://otherserver/foo/bar/")
Отключение проверки ALLOWED_HOSTS ( ALLOWED_HOSTS = [‘*’] ) при выполнении тестов предотвращает выдачу тестовым клиентом полезного сообщения об ошибке, если вы следуете перенаправлению на внешний URL.
Тесты и многочисленные базы данных¶
Тестирование конфигураций основной/реплики¶
Если вы тестируете конфигурацию из нескольких баз данных с репликацией первичной/репликативной (в некоторых базах данных называемой ведущей/ведомой), такая стратегия создания тестовых баз данных создает проблему. Когда создаются тестовые базы данных, репликации не будет, и в результате данные, созданные на основной базе данных, не будут видны на реплике.
Чтобы компенсировать это, Django позволяет вам определить, что база данных является тестовым зеркалом. Рассмотрим следующий (упрощенный) пример конфигурации базы данных:
DATABASES = "default": "ENGINE": "django.db.backends.mysql", "NAME": "myproject", "HOST": "dbprimary", # . plus some other settings >, "replica": "ENGINE": "django.db.backends.mysql", "NAME": "myproject", "HOST": "dbreplica", "TEST": "MIRROR": "default", >, # . plus some other settings >, >
В этой установке у нас есть два сервера баз данных: dbprimary , описанный псевдонимом базы данных default , и dbreplica , описанный псевдонимом replica . Как и следовало ожидать, dbreplica был настроен администратором базы данных как копия для чтения dbprimary , поэтому при нормальной работе любая запись на default будет отображаться на replica .
Если бы Django создал две независимые тестовые базы данных, это нарушило бы все тесты, которые ожидали репликации. Однако, база данных replica была настроена как тестовое зеркало (с помощью настройки MIRROR ), что указывает на то, что при тестировании база данных replica должна рассматриваться как зеркало базы данных default .
При настройке тестовой среды тестовая версия replica не будет создана. Вместо этого соединение с replica будет перенаправлено на default . В результате записи в default будут появляться в replica — но потому, что это фактически одна и та же база данных, а не потому, что между двумя базами данных существует репликация данных. Поскольку это зависит от транзакций, в тестах необходимо использовать TransactionTestCase , а не TestCase .
Управление порядком создания тестовых баз данных¶
По умолчанию Django предполагает, что все базы данных зависят от базы данных default и поэтому всегда создает базу данных default первой. Однако не гарантируется порядок создания любых других баз данных в вашей тестовой установке.
Если конфигурация вашей базы данных требует определенного порядка создания, вы можете указать существующие зависимости с помощью тестовой настройки DEPENDENCIES . Рассмотрим следующий (упрощенный) пример конфигурации базы данных:
DATABASES = "default": # . db settings "TEST": "DEPENDENCIES": ["diamonds"], >, >, "diamonds": # . db settings "TEST": "DEPENDENCIES": [], >, >, "clubs": # . db settings "TEST": "DEPENDENCIES": ["diamonds"], >, >, "spades": # . db settings "TEST": "DEPENDENCIES": ["diamonds", "hearts"], >, >, "hearts": # . db settings "TEST": "DEPENDENCIES": ["diamonds", "clubs"], >, >, >
При такой конфигурации база данных diamonds будет создана первой, так как это единственный псевдоним базы данных без зависимостей. Далее будут созданы псевдонимы default и clubs (хотя порядок создания этой пары не гарантирован), затем hearts и, наконец, spades .
Если в определении DEPENDENCIES есть какие-либо круговые зависимости, будет вызвано исключение ImproperlyConfigured .
Расширенные возможности TransactionTestCase ¶
TransactionTestCase. available_apps ¶
Этот атрибут является частным API. В будущем он может быть изменен или удален без указания срока амортизации, например, для учета изменений в загрузке приложений.
Он используется для оптимизации собственного набора тестов Django, который содержит сотни моделей, но не имеет связей между моделями в разных приложениях.
По умолчанию available_apps устанавливается на None . После каждого теста Django вызывает flush , чтобы сбросить состояние базы данных. Это опустошает все таблицы и выдает сигнал post_migrate , который воссоздает один тип содержимого и четыре разрешения для каждой модели. Эта операция становится дорогой пропорционально количеству моделей.
Установка available_apps в список приложений инструктирует Django вести себя так, как будто доступны только модели из этих приложений. Поведение TransactionTestCase изменяется следующим образом:
- post_migrate запускается перед каждым тестом для создания типов содержимого и разрешений для каждой модели в доступных приложениях, в случае их отсутствия.
- После каждого теста Django очищает только таблицы, соответствующие моделям в доступных приложениях. Однако, на уровне базы данных, усечение может каскадировать на связанные модели в недоступных приложениях. Кроме того, post_migrate не срабатывает; он сработает после следующего TransactionTestCase , когда будет выбран правильный набор приложений.
Поскольку база данных не полностью очищена, если тест создает экземпляры моделей, не включенных в available_apps , они будут утекать и могут привести к неудаче несвязанных тестов. Будьте осторожны с тестами, использующими сессии; механизм сессий по умолчанию хранит их в базе данных.
Поскольку post_migrate не выдается после очистки базы данных, его состояние после TransactionTestCase не такое же, как после TestCase : в нем отсутствуют строки, созданные слушателями post_migrate . Учитывая order in which tests are executed , это не является проблемой, если либо все TransactionTestCase в данном тестовом наборе объявляют available_apps , либо ни один из них.
available_apps является обязательным в собственном наборе тестов Django.
Установка reset_sequences = True на TransactionTestCase будет гарантировать, что последовательности всегда сбрасываются перед выполнением теста:
class TestsThatDependsOnPrimaryKeySequences(TransactionTestCase): reset_sequences = True def test_animal_pk(self): lion = Animal.objects.create(name="lion", sound="roar") # lion.pk is guaranteed to always be 1 self.assertEqual(lion.pk, 1)
Если вы явно не тестируете порядковые номера первичных ключей, рекомендуется не кодировать значения первичных ключей в тестах.
Использование reset_sequences = True замедлит выполнение теста, поскольку сброс первичного ключа является относительно дорогой операцией базы данных.
Обеспечение последовательного запуска тестовых классов¶
Если у вас есть тестовые классы, которые нельзя запускать параллельно (например, потому что они используют общий ресурс), вы можете использовать django.test.testcases.SerializeMixin для их последовательного запуска. Этот миксин использует файловую систему lockfile .
Например, вы можете использовать __file__ , чтобы определить, что все тестовые классы в том же файле, которые наследуются от SerializeMixin , будут запускаться последовательно:
import os from django.test import TestCase from django.test.testcases import SerializeMixin class ImageTestCaseMixin(SerializeMixin): lockfile = __file__ def setUp(self): self.filename = os.path.join(temp_storage_dir, "my_file.png") self.file = create_file(self.filename) class RemoveImageTests(ImageTestCaseMixin, TestCase): def test_remove_image(self): os.remove(self.filename) self.assertFalse(os.path.exists(self.filename)) class ResizeImageTests(ImageTestCaseMixin, TestCase): def test_resize_image(self): resize_image(self.file, (48, 48)) self.assertEqual(get_image_size(self.file), (48, 48))
Использование бегуна тестирования Django для тестирования многократно используемых приложений¶
Если вы пишете reusable application , вы, возможно, захотите использовать Django test runner, чтобы запустить свой собственный набор тестов и таким образом воспользоваться инфраструктурой тестирования Django.
Обычно рядом с кодом приложения располагается каталог tests, имеющий следующую структуру:
runtests.py polls/ __init__.py models.py . tests/ __init__.py models.py test_settings.py tests.py
Давайте заглянем в несколько таких файлов:
runtests.py ¶
#!/usr/bin/env python import os import sys import django from django.conf import settings from django.test.utils import get_runner if __name__ == "__main__": os.environ["DJANGO_SETTINGS_MODULE"] = "tests.test_settings" django.setup() TestRunner = get_runner(settings) test_runner = TestRunner() failures = test_runner.run_tests(["tests"]) sys.exit(bool(failures))
Это сценарий, который вы вызываете для запуска набора тестов. Он устанавливает окружение Django, создает тестовую базу данных и запускает тесты.
Для ясности, этот пример содержит только необходимый минимум для использования бегуна тестирования Django. Возможно, вы захотите добавить опции командной строки для контроля многословности, передачи определенных тестовых меток для запуска и т.д.
tests/test_settings.py ¶
SECRET_KEY = "fake-key" INSTALLED_APPS = [ "tests", ]
Этот файл содержит Django settings , необходимые для запуска тестов вашего приложения.
Опять же, это минимальный пример; для запуска ваших тестов могут потребоваться дополнительные настройки.
Поскольку пакет tests включается в INSTALLED_APPS при запуске ваших тестов, вы можете определить модели только для тестов в его models.py файле.
Использование различных фреймворков для тестирования¶
Очевидно, что unittest — не единственный фреймворк тестирования Python. Хотя Django не предоставляет явной поддержки альтернативных фреймворков, он предоставляет возможность вызывать тесты, созданные для альтернативного фреймворка, как если бы это были обычные тесты Django.
Когда вы выполняете ./manage.py test , Django смотрит на параметр TEST_RUNNER , чтобы определить, что делать. По умолчанию TEST_RUNNER указывает на ‘django.test.runner.DiscoverRunner’ . Этот класс определяет поведение тестирования Django по умолчанию. Это поведение включает в себя:
- Выполнение глобальной предтестовой настройки.
- Ищет тесты в любом файле ниже текущего каталога, имя которого соответствует шаблону test*.py .
- Создание тестовых баз данных.
- Запуск migrate для установки моделей и начальных данных в тестовые базы данных.
- Выполнение system checks .
- Запуск найденных тестов.
- Уничтожение тестовых баз данных.
- Проведение глобального послетестового разбора.
Если вы определите свой собственный класс тестового бегуна и укажете TEST_RUNNER на этот класс, Django будет выполнять ваш тестовый бегун всякий раз, когда вы запускаете ./manage.py test . Таким образом, можно использовать любой тестовый фреймворк, который может быть выполнен из кода Python, или модифицировать процесс выполнения тестов Django, чтобы удовлетворить любые требования к тестированию, которые у вас могут быть.
Определение программы запуска тестов¶
Бегунок для тестирования — это класс, определяющий метод run_tests() . Django поставляется с классом DiscoverRunner , который определяет поведение тестирования Django по умолчанию. Этот класс определяет точку входа run_tests() , а также ряд других методов, которые используются run_tests() для установки, выполнения и разрушения набора тестов.
class DiscoverRunner ( pattern = ‘test*.py’ , top_level = None , verbosity = 1 , interactive = True , failfast = False , keepdb = False , reverse = False , debug_mode = False , debug_sql = False , parallel = 0 , tags = None , exclude_tags = None , test_name_patterns = None , pdb = False , buffer = False , enable_faulthandler = True , timing = True , shuffle = False , logger = None , durations = None , ** kwargs ) [исходный код] ¶
DiscoverRunner будет искать тесты в любом файле, соответствующем pattern .
top_level можно использовать для указания каталога, содержащего ваши модули Python верхнего уровня. Обычно Django определяет это автоматически, поэтому указывать этот параметр не обязательно. Если он указан, то, как правило, это должен быть каталог, содержащий ваш файл manage.py .
verbosity определяет количество уведомлений и отладочной информации, которая будет выводиться на консоль; 0 — нет вывода, 1 — нормальный вывод, 2 — подробный вывод.
Если interactive равно True , тестовый пакет имеет разрешение запрашивать у пользователя инструкции при выполнении тестового пакета. Примером такого поведения может быть запрос разрешения на удаление существующей базы данных тестов. Если interactive равно False , тестовый пакет должен иметь возможность запускаться без какого-либо ручного вмешательства.
Если failfast равно True , набор тестов прекратит выполнение после обнаружения первого сбоя теста.
Если keepdb равно True , тестовый пакет будет использовать существующую базу данных или создаст ее при необходимости. Если False , будет создана новая база данных, при этом пользователю будет предложено удалить существующую базу данных, если таковая имеется.
Если reverse равно True , тестовые случаи будут выполняться в обратном порядке. Это может быть полезно для отладки тестов, которые не изолированы должным образом и имеют побочные эффекты. Grouping by test class сохраняется при использовании этой опции. Эту опцию можно использовать вместе с —shuffle , чтобы изменить порядок для определенного случайного семени.
debug_mode указывает, на что должна быть установлена настройка DEBUG перед запуском тестов.
parallel задает количество процессов. Если parallel больше 1 , то тестовый набор будет выполняться в parallel процессах. Если классов тестовых примеров меньше, чем настроенных процессов, Django соответственно уменьшит количество процессов. Каждый процесс получает свою собственную базу данных. Для корректного отображения трассировки эта опция требует использования стороннего пакета tblib .
tags может использоваться для указания набора tags for filtering tests . Может комбинироваться с exclude_tags .
exclude_tags может использоваться для указания набора tags for excluding tests . Может комбинироваться с tags .
Если debug_sql равно True , то в случае неудачных тестов будут выведены SQL-запросы, записанные в журнал django.db.backends logger , а также обратная трассировка. Если verbosity равно 2 , то выводятся запросы во всех тестах.
test_name_patterns можно использовать для задания набора шаблонов для фильтрации тестовых методов и классов по их именам.
Если pdb равно True , отладчик ( pdb или ipdb ) будет порождаться при каждой ошибке или сбое теста.
Если buffer равно True , выводы из пройденных тестов будут отброшены.
Если enable_faulthandler равно True , то faulthandler будет включен.
Если timing имеет значение True , будет показано время выполнения теста, включая настройку базы данных и общее время работы.
Если shuffle является целым числом, тестовые случаи будут перемешаны в случайном порядке перед выполнением, используя целое число в качестве случайной затравки. Если shuffle равно None , семя будет генерироваться случайным образом. В обоих случаях семя будет зарегистрировано и установлено в значение self.shuffle_seed перед запуском тестов. Эта опция может использоваться для обнаружения тестов, которые не изолированы должным образом. Grouping by test class сохраняется при использовании этой опции.
logger можно использовать для передачи Python Logger object . Если передано, то логгер будет использоваться для регистрации сообщений вместо печати на консоль. Объект логгера будет соблюдать свой уровень протоколирования, а не verbosity .
durations покажет список из N самых медленных тестовых случаев. При установке этого параметра в значение 0 будет показана продолжительность всех тестов. Требуется Python 3.12+.
Django может время от времени расширять возможности бегуна тестирования, добавляя новые аргументы. Объявление **kwargs позволяет это расширение. Если вы подкласс DiscoverRunner или пишете свой собственный бегунок тестирования, убедитесь, что он принимает **kwargs .
Ваша программа запуска тестов может также определять дополнительные параметры командной строки. Создайте или переопределите метод класса add_arguments(cls, parser) и добавьте пользовательские аргументы, вызвав parser.add_argument() внутри метода, чтобы команда test могла использовать эти аргументы.
New in Django 5.0:
Был добавлен аргумент durations .
Атрибуты¶
DiscoverRunner. test_suite ¶
Класс, используемый для построения набора тестов. По умолчанию он имеет значение unittest.TestSuite . Его можно переопределить, если вы хотите реализовать другую логику для сбора тестов.
Это класс низкоуровневой программы запуска тестов, которая используется для выполнения отдельных тестов и форматирования результатов. По умолчанию он имеет значение unittest.TextTestRunner . Несмотря на досадное сходство в именовании, это не тот же тип класса, что DiscoverRunner , который охватывает более широкий набор обязанностей. Вы можете переопределить этот атрибут, чтобы изменить способ выполнения тестов и создания отчетов.
Это класс, который загружает тесты, будь то из TestCases, модулей или иным образом, и собирает их в наборы тестов для выполнения бегуном. По умолчанию он установлен на unittest.defaultTestLoader . Вы можете переопределить этот атрибут, если ваши тесты будут загружаться необычным образом.
Методы¶
DiscoverRunner. run_tests ( test_labels , ** kwargs ) [исходный код] ¶
Запустите набор тестов.
test_labels позволяет указать, какие тесты запускать, и поддерживает несколько форматов (см. DiscoverRunner.build_suite() для списка поддерживаемых форматов).
Этот метод должен возвращать количество тестов, которые не прошли.
classmethod DiscoverRunner. add_arguments ( parser ) [исходный код] ¶
Переопределите этот метод класса, чтобы добавить пользовательские аргументы, принимаемые командой управления test . Подробности о добавлении аргументов в синтаксический анализатор см. в argparse.ArgumentParser.add_argument() .
DiscoverRunner. setup_test_environment ( ** kwargs ) [исходный код] ¶
Устанавливает тестовую среду, вызывая setup_test_environment() и устанавливая DEBUG в self.debug_mode (по умолчанию False ).
DiscoverRunner. build_suite ( test_labels = None , ** kwargs ) [исходный код] ¶
Создает набор тестов, соответствующий предоставленным тестовым меткам.
test_labels — это список строк, описывающих тесты, которые будут запущены. Метка теста может принимать одну из четырех форм:
- path.to.test_module.TestCase.test_method – Запуск одного тестового метода в классе тестовых примеров.
- path.to.test_module.TestCase – Запуск всех методов тестирования в тестовом примере.
- path.to.module – Поиск и запуск всех тестов в названном пакете или модуле Python.
- path/to/directory – Поиск и запуск всех тестов ниже именованной директории.
Если test_labels имеет значение None , бегунок тестирования будет искать тесты во всех файлах ниже текущего каталога, имена которых совпадают с его pattern (см. выше).
Возвращает экземпляр TestSuite , готовый к выполнению.
DiscoverRunner. setup_databases ( ** kwargs ) [исходный код] ¶
Создает тестовые базы данных, вызывая setup_databases() .
DiscoverRunner. run_checks ( databases ) [исходный код] ¶
Выполняет system checks на тестовом databases .
DiscoverRunner. run_suite ( suite , ** kwargs ) [исходный код] ¶
Запускает набор тестов.
Возвращает результат, полученный в результате выполнения набора тестов.
Возвращает аргументы ключевых слов для инстанцирования DiscoverRunner.test_runner .
DiscoverRunner. teardown_databases ( old_config , ** kwargs ) [исходный код] ¶
Уничтожает тестовые базы данных, восстанавливая предтестовые условия вызовом teardown_databases() .
DiscoverRunner. teardown_test_environment ( ** kwargs ) [исходный код] ¶
Восстанавливает среду предварительного тестирования.
DiscoverRunner. suite_result ( suite , result , ** kwargs ) [исходный код] ¶
Вычисляет и возвращает код возврата на основе набора тестов и результата этого набора тестов.
Если задано значение logger , записывает сообщение в журнал на заданное целое число logging level (например, logging.DEBUG , logging.INFO или logging.WARNING ). В противном случае сообщение выводится на консоль с соблюдением текущего verbosity . Например, сообщение не будет напечатано, если verbosity равно 0, INFO и выше будет напечатано, если verbosity равно хотя бы 1, и DEBUG будет напечатано, если оно равно хотя бы 2. level по умолчанию равно logging.INFO .
Утилиты для тестирования¶
django.test.utils ¶
Чтобы помочь в создании собственной программы запуска тестов, Django предоставляет ряд полезных методов в модуле django.test.utils .
setup_test_environment ( debug = None ) [исходный код] ¶
Выполняет глобальную настройку перед тестированием, например, устанавливает инструментарий для системы рендеринга шаблонов и настраивает макет почтового ящика.
Если debug не является None , то параметр DEBUG обновляется до его значения.
Выполняет глобальное устранение последствий тестирования, например, удаление инструментария из системы шаблонов и восстановление нормальной работы почтовых служб.
setup_databases ( verbosity , interactive , * , time_keeper = None , keepdb = False , debug_sql = False , parallel = 0 , aliases = None , serialized_aliases = None , ** kwargs ) [исходный код] ¶
Создает тестовые базы данных.
Возвращает структуру данных, содержащую достаточно подробную информацию, чтобы отменить внесенные изменения. Эти данные будут предоставлены функции teardown_databases() по завершении тестирования.
Аргумент aliases определяет, для каких DATABASES алиасов должны быть настроены тестовые базы данных. Если он не указан, то по умолчанию используются все DATABASES псевдонимы.
Аргумент serialized_aliases определяет, какое подмножество тестовых баз данных aliases должно иметь сериализованное состояние для использования функции serialized_rollback . Если он не указан, то по умолчанию используется значение aliases .
teardown_databases ( old_config , parallel = 0 , keepdb = False ) [исходный код] ¶
Уничтожает тестовые базы данных, восстанавливая условия до тестирования.
old_config — это структура данных, определяющая изменения в конфигурации базы данных, которые необходимо отменить. Это возвращаемое значение метода setup_databases() .
django.db.connection.creation ¶
Модуль создания бэкенда базы данных также предоставляет некоторые утилиты, которые могут быть полезны во время тестирования.
create_test_db ( verbosity = 1 , autoclobber = False , serialize = True , keepdb = False )¶
Создает новую тестовую базу данных и запускает migrate против нее.
verbosity имеет такое же поведение, как и run_tests() .
autoclobber описывает поведение, которое произойдет, если будет обнаружена база данных с тем же именем, что и тестовая база данных:
- Если autoclobber равно False , пользователю будет предложено одобрить уничтожение существующей базы данных. Если пользователь не одобрил, вызывается sys.exit .
- Если autoclobber равно True , база данных будет уничтожена без согласования с пользователем.
serialize определяет, сериализует ли Django базу данных в JSON-строку в памяти перед запуском тестов (используется для восстановления состояния базы данных между тестами, если у вас нет транзакций). Вы можете установить значение False , чтобы ускорить время создания тестов, если у вас нет тестовых классов с serialized_rollback=True .
keepdb определяет, следует ли при выполнении теста использовать существующую базу данных или создать новую. Если True , будет использована существующая база данных или создана, если ее нет. Если False , будет создана новая база данных, при этом пользователю будет предложено удалить существующую базу данных, если таковая имеется.
Возвращает имя созданной тестовой базы данных.
create_test_db() имеет побочный эффект изменения значения NAME в DATABASES , чтобы оно соответствовало имени тестовой базы данных.
destroy_test_db ( old_database_name , verbosity = 1 , keepdb = False )¶
Уничтожает базу данных, имя которой является значением NAME в DATABASES , и устанавливает NAME в значение old_database_name .
Аргумент verbosity имеет такое же поведение, как и для DiscoverRunner .
Если аргумент keepdb равен True , то соединение с базой данных будет закрыто, но база данных не будет уничтожена.
Интеграция с coverage.py ¶
Покрытие кода описывает, сколько исходного кода было протестировано. Оно показывает, какие части вашего кода проверяются тестами, а какие нет. Это важная часть тестирования приложений, поэтому настоятельно рекомендуется проверять покрытие ваших тестов.
Django может быть легко интегрирован с coverage.py, инструментом для измерения покрытия кода Python-программ. Сначала установите coverage. Затем из папки проекта, содержащей manage.py , выполните следующие действия:
coverage run --source='.' manage.py test myapp
При этом запускаются тесты и собираются данные о покрытии выполняемых файлов в проекте. Отчет по этим данным можно посмотреть, выполнив следующую команду:
coverage report
Обратите внимание, что во время выполнения тестов был выполнен некоторый код Django, но он не указан здесь из-за флага source , переданного предыдущей команде.
Для получения дополнительных возможностей, таких как аннотированные HTML-листинги с подробным описанием пропущенных строк, смотрите документацию coverage.py.
Как тестировать сайт на Django. Часть 3. Отправка результата на почту, TestExplorer и декоратор tag
Тесты написаны, тимлид рад, а что дальше-то делать? А дальше – автоматизация и отправка отчёта по тестам. Именно об этом мы поговорим в данной статье, попутно затронув полезный инструмент TestExplorer и декоратор tag.
Прошлые части статьи
Раз уж вы зашли на эту статью, думаю, тесты вам уже приходилось писать. Но, если хотите узнать о них больше, предлагаю посетить первую и вторую части. В них я на примере кода нашего сайта, рассказывают о тестировании таких веб-систем.
TestExplorer
TestExplorer — это расширение, позволяющее взаимодействовать с тестами не через консоль, а напрямую в вашей IDE. Можно сказать, что это GUI для прогона тестов.
TestExplorer встроен в PyCharm. Про его использование в этом редакторе можно почитать тут. VS Code, напротив, требует отдельной установки и настройки данного расширения. Рассмотрим, как это можно сделать.
Настраиваем TestExplorer для Django в VS Code
Установка
Скачиваем расширение Python Test Explorer for Visual Studio Code. Вместе с ним автоматически должно установиться ещё одно — Test Explorer UI. Поставьте его вручную, если этого не произошло.
Настройка. Способ 1
Настроить TestExplorer для работы с Django можно двумя способами. Начнём с первого. Он больше подходит для тестов конкретных приложений (а не всего проекта), либо для тестирования с помощью unittest.
Создадим обычный Django-проект my_project с приложением my_app. Открываем файл __init__.py в директории my_app и добавляем в него следующий код:
from os import environ from django import setup environ.setdefault( 'DJANGO_SETTINGS_MODULE', 'my_project.settings' ) setup()
TestExplorer берёт из нашего проекта файлы с тестами. Но где взять для них настройки, он не знает. Поэтому мы создаём переменную окружения DJANGO_SETTINGS_MODULE и с помощью метода setup передаём её в TestExplorer.
Далее нам нужно определиться с тестовым фреймворком. Обычно это либо pytest, либо unittest. О разнице между ними можно почитать тут. unittest встроен в Python, поэтому устанавливать его нам не придётся. Для инсталляции pytest вводим эту команду в консоли:
pip install -U pytest
Теперь нам нужно «собрать» тесты. Открываем Command palette (Ctrl + Shift + P), вводим «Python: Configure Tests» и жмём Enter.
Выбираем нужный фреймворк и затем директорию с тестами — my_app. Если вы выбрали unittest, то вам нужно указать паттерн, по которому будут искаться тесты. В нашем случае это «test_.py».
Настройка. Способ 2
У Python TestExplorer есть специальный пакет pytest-django. С его помощью можно легко настроить работу TestExplorer через pytest для всего проекта, а не отдельного приложения. Сначала установим его:
pip install pytest-django
Далее создадим в нашем проекте файл pytest.ini с таким кодом:
[pytest] DJANGO_SETTINGS_MODULE = my_project.settings python_files = test_.py
Мы указываем, какой фреймворк использовать, где брать настройки проекта и по какому шаблону искать тесты. Теперь остаётся выполнить «сборку» тестов, как в первом способе с помощью «Python: Configure Tests».
Запуск тестов
Теперь, когда всё готово, начнём работать с тестами. Слева на панели Activity Bar жмём на значок колбы.
Откроется панель «Testing». В верхней её части отображается вся иерархия тестов проекта. В нижней — все файлы с тестами. Если вы написали все тесты из первой статьи, то панель «Testing» будет выглядеть так:
Наводим мышку на фразу «TEST EXPLORER» в любой из частей панели и видим справа блок с кнопками. Жмём кнопку «Run Tests» (треугольник), и все наши тесты начинают прогоняться. При успешном выполнении иерархия тестов будет выглядеть так:
Обзор возможностей TestExplorer
Обе панели TestExplorer позволяют:
- Запускать все тесты в обычном режиме;
- Запускать конкретный тест в отладочном режиме;
- Сбрасывать результаты тестов;
- Сортировать тесты;
- Переходить к коду конкретного теста;
- Получать время прогона тестов.
С помощью верхней панели можно:
- Искать конкретные тесты, применяя фильтры;
- Отображать output выполнения тестов;
- Отображать тесты в виде иерархии, либо в виде списка;
- Прогонять только упавшие тесты;
- Прогонять все тесты в режиме отладки;
- Скрыть тест из панели;
- Узнать, сколько тестов выполнено успешно (в процентах либо в количестве тестов).
Нижняя панель не такая функциональная, но она позволяет включить автопрогон конкретных тестовых файлов. Т.е. выбранные тесты будут автоматически выполняться при изменении любого кода проекта.
Советую поиграться с этими панелями, чтобы лучше понять их возможности.
Теперь зайдём на любой тест. Над названием метода появилась небольшая панель ссылок. С её помощью можно запускать и дебажить тест, не уходя с его кода. Также слева от номера строки можно заметить галочку. Это статус выполнения теста. В данном случае — успешный.
При нажатии ЛКМ на галочку тест прогонится ещё раз. Если нажать ПКМ, откроется расширенный список действий, которые можно выполнить над тестом.
Запуск TestExplorer в контейнере Docker
Сейчас довольно много проектов используют Docker и Docker Compose. Если не все зависимости из requirements.txt установлены на машине, то просто так запустить TestExplorer для такого проекта не получится. С этой проблемой я лично столкнулся при написании тестов для нашего сайта. Давайте разберём, как её решить.
Чтобы было, над чем ставить опыты, создадим обычный Django-проект с названием proj и добавим в него приложение app. Из приложения удаляем файл tests.py и добавляем папку tests. В ней создаём файл test_views.py с максимально простым тестом:
from django.test import TestCase class ViewsTests(TestCase): def test_true(self): self.assertTrue(True)
Добавим файл pytest.ini в корень проекта.
[pytest] DJANGO_SETTINGS_MODULE = proj.settings python_files = test_*.py
Соберём все тесты через Command Palette, как было показано выше. И запустим TestExplorer. Результат будет таким:
Тест работает. Теперь представим, что у нас большой проект с множеством зависимостей. Сымитируем их одной библиотекой — faust. На её месте может быть любая другая, не установленная у вас на машине. Для имитации добавим в settings.py проекта данную строку:
import faust
Теперь при запуске сервера мы получаем ошибку ModuleNotFoundError, что вполне логично. TestExplorer тоже говорит о проблеме и выдаёт ImportErorr:
Поскольку библиотека faust — имитация множества библиотек, устанавливать её на локальную машину будет проблематично. Поэтому воспользуемся услугами Docker Compose. Для этого добавим в корень проекта 3 файла.
Django>=3.0,=2.8 faust>=1.10.4
FROM python:3 ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 WORKDIR /code COPY requirements.txt /code/ RUN pip install -r requirements.txt COPY . /code/
version: "3" services: web: build: . command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - "8000:8000"
docker-compose up
TestExplorer по-прежнему не работает, поскольку он запускается локально, а не через контейнер. А значит, использует только те зависимости, которые установлены на нашу машину. Чтобы заставить TestExplorer работать, нужно запустить его внутри контейнера. Для этого сначала надо к нему подключиться. VS Code для этого предлагает специальное расширение Docker. После его установки слева на панели вы увидите соответствующую иконку:
Жмём на неё и видим все запущенные контейнеры.
Жмём на название нашего контейнера и выбираем «Attach Visual Studio Code». Таким образом мы подключимся к контейнеру с помощью VS Code.
Чтобы увидеть содержимое контейнера, нужно открыть папку code. Именно в ней оно и хранится (мы указали это в Dockerfile). Для открытия папки используем команду Ctrl K + Ctrl O.
Для дальнейшей работы, возможно, придётся установить расширение Python прямо в контейнер. Затем нужно выбрать версию его интерпретатора в Command Palette. Лучше выбирать самую свежую.
Установим pytest-django в контейнер:
pip install pytest-django
И выполним сбор тестов с помощью флага collect-only:
python -m pytest --collect-only
Запускаем тесты и убеждаемся, что всё работает.
Запуск конкретных тестов
Допустим, вы нашли 404-ю ошибку на сайте и хотите узнать, есть ли ещё такие страницы. Тест на статус-код поможет это проверить. Но вы понимаете, что запускать все тесты ради одного — долго и неудобно. И как тогда быть? Конечно, можно воспользоваться TestExplorer, но, допустим, вам нужен консольный запуск тестов. Тогда можно закомментировать ненужные тесты или повесить на них skip. Но лучше воспользоваться декоратором tag. В него передаётся один или несколько идентификаторов, по которым можно обратиться к конкретному тесту.
@tag('идентификатор 1', ['идентификатор 2'], [. ])
Возьмём наши тесты для пользователя-тестировщика из первой статьи и добавим к ним теги.
from django.test import tag @tag('status_code') def test_status_code(self): # . @tag('links', 'links_and_redirects') def test_links(self): # . @tag('redirects', 'links_and_redirects') def test_redirects(self): # .
Чтобы указать тег при запуске тестов, надо добавить конструкцию —tag= после основной команды.
python manage.py test --tag=
Теперь немного поиграемся.
Запуск только тестов на статус-код:
python manage.py test --tag=status_code
Запуск только тестов на ссылки:
python manage.py test --tag=links
Запуск только тестов на ссылки и редиректы (вариант 1):
python manage.py test --tag=links_and_redirects
Запуск только тестов на ссылки и редиректы (вариант 2):
python manage.py test --tag=links --tag=redirects
Сейчас мы вызываем конкретный тест или конкретную группу тестов. Но что если мы хотим запустить, наоборот, все тесты, кроме некоторых? Например, кроме тестов на редиректы. Команда —exclude-tag поможет нам это сделать.
python manage.py test --exclude-tag=redirects
Отправка результата выполнения тестов на почту
Каждый раз запускать тесты вручную — не самый лучший вариант. Гораздо удобнее, если тесты будут запускаться автоматически. А ещё лучше, чтобы результат их выполнения приходил нам на почту. Как это сделать, мы и разберём далее.
Самый простой способ автоматизации тестов — использование крона. Напишем скрипт, который будет выполнять тесты и отправлять их результат на почту.
Полный код скрипта
from smtplib import SMTP from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from subprocess import STDOUT, PIPE, Popen from decouple import config from emoji import emojize class TestLauncher: """Класс запуска тестов""" __EMAILS = ('ваша почта',) __COMMAND = '/usr/bin/python3 manage.py test --noinput' __REPORT_SUBJECT = 'Результаты прогона тестов' __SUCCESS_STATUS = emojize( ":check_mark_button: Тесты выполнены успешно :check_mark_button:" \ "\n:man_dancing_medium-dark_skin_tone: Пляшем" \ ":woman_dancing_light_skin_tone:\n" ) __FAILURE_STATUS = emojize( ":cross_mark: Тесты упали :cross_mark:\n" \ "Полный печалити :weary_cat:\n" ) def __init__(self): self.__process = Popen( self.__COMMAND.split(), stdout=PIPE, stderr=STDOUT ) self.__output = iter(self.__process.stdout.readline, b'') self.__description = self.__get_description() self.__process.communicate() self.__status_code = self.__process.returncode def __is_successful(self) -> bool: """Если код запуска тестов - 0, тесты выполнены успешно""" return self.__status_code == 0 def __get_description(self) -> str: """Описание выполнения тестов, взятое из output""" return ''.join(line.decode() + '\n' for line in self.__output) def send_report(self) -> None: """С помощью mailgun отправляет отчёт на указанные почты""" smtp_object = SMTP('smtp.eu.mailgun.org', 587) smtp_object.starttls() smtp_object.login( config('EMAIL_HOST_USER'), config('EMAIL_HOST_PASSWORD') ) smtp_object.sendmail( config('TESTS_REPORT_SENDER_ADDRESS'), self.__EMAILS, self.__report.as_string() ) smtp_object.quit() @property def __status(self) -> str: """Статус выполнения тестов""" return ( self.__SUCCESS_STATUS if self.__is_successful() else self.__FAILURE_STATUS ) @property def __body(self) -> str: """Полное содержимое отчёта о выполнении тестов""" body = f'
'.replace('\n', '
') return MIMEText(body, 'html') @property def __report(self) -> MIMEMultipart: """Отчёт о выполнении тестов""" report = MIMEMultipart() report['Subject'] = self.__REPORT_SUBJECT report.attach(self.__body) return report if __name__ == '__main__': test_launcher = TestLauncher() test_launcher.send_report()
Рассмотрим код детально.
Импорты
Для отправки сообщений на почту мы используем класс SMTP встроенной библиотеки smtplib.
from smtplib import SMTP
Отчёт о выполнении тестов имеет MIME-тип multipart, а его содержимое — MIME-тип text. Их мы задаём с помощью классов MIMEMultipart и MIMEText.
from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText
Класс Popen используется для выполнения программы в отдельном процессе. В нашем случае он запускает тесты и выдаёт их результат. STDOUT и PIPE передаются ему в качестве аргументов (об этом позже).
from subprocess import STDOUT, PIPE, Popen
Переменные окружения прописаны в файле .env. Для их получения используем функцию config.
from decouple import config
Чтобы немного разукрасить тестовый отчёт, добавим в него эмоджи (почему бы и нет) с помощью функции emojize.
from emoji import emojize
Поля класса TestLauncher
Все нужные методы и данные хранятся в одном классе — TestLauncher. Опишем его поля.
Кортеж с почтами, на которые будет выполняться отправка отчёта:
__EMAILS = ('ваша почта',)
Команда, запускающая наши тесты:
__COMMAND = '/usr/bin/python3 manage.py test --noinput'
Если после предыдущего тестирования база не удалилась, флаг noinput скажет сделать это без запроса на разрешение. Путь к Python у вас может быть другим. Чтобы узнать его, введите следующую команду в консоли:
which python3
Тема отправляемого на почту сообщения:
__REPORT_SUBJECT = 'Результаты прогона тестов'
Статусы выполнения тестов:
__SUCCESS_STATUS = emojize( ":check_mark_button: Тесты выполнены успешно :check_mark_button:" \ "\n:man_dancing_medium-dark_skin_tone: Пляшем" \ ":woman_dancing_light_skin_tone:\n" )
__FAILURE_STATUS = emojize( ":cross_mark: Тесты упали :cross_mark:\n" \ "Полный печалити :weary_cat:\n" )
Они будут отображаться в теле сообщения. Чтобы немного их разукрасить, мы добавляем несколько эмоджи, используя функцию emojize.
Метод __init__
Переходим к методам. Начнём с __init__. Его действия следующие:
- Создаёт процесс, выполняющий запуск тестов. Первым аргументом он принимает нашу команду, разделённую на список. Чтобы в дальнейшем мы могли использовать результат выполнения тестов, указываем значение PIPE для stdout. А чтобы все ошибки шли в stdout, указываем для stderr значение STDOUT;
- Получает результат выполнения тестов из stdout;
- С помощью метода __get_description (будет рассмотрен позже) сохраняет описание выполнения тестов в переменную;
- Ждёт завершения процесса и записывает код его выполнения в атрибут returncode. Если он равен 0, значит всё прошло хорошо, и тесты выполнены успешно;
- Заносит код выполнения процесса в переменную __status_code.
def __init__(self): self.__process = Popen( self.__COMMAND.split(), stdout=PIPE, stderr=STDOUT ) # (1) self.__output = iter(self.__process.stdout.readline, b'') # (2) self.__description = self.__get_description() # (3) self.__process.communicate() # (4) self.__status_code = self.__process.returncode # (5)
Метод __is_successful
Как говорилось выше, если статус-код процесса — 0, значит, тесты выполнились успешно. Это проверяет метод __is_successful.
def __is_successful(self) -> bool: """Если код запуска тестов - 0, тесты выполнены успешно""" return self.__status_code == 0
Метод __get_description
Следующий метод выдаёт описание выполнения тестов. Он берёт его из output процесса, созданного в __init__. Каждая строка описания декодируется из байтовой в обычную, затем к ней добавляется «\n». После все строки объединяются через join.
def __get_description(self) -> str: """Описание выполнения тестов, взятое из output""" return ''.join(line.decode() + '\n' for line in self.__output)
Метод send_report
Теперь рассмотрим метод send_report. Вот его действия:
- Подключается к серверу mailgun и создаёт SMTP объект;
- Указывает, что соединение должно шифроваться с помощью TLS;
- Авторизуется под конкретным пользователем;
- Отправляет отчёт о тестах на указанные почты;
- Закрывает соединение.
def send_report(self) -> None: """С помощью mailgun отправляет отчёт на указанные почты""" smtp_object = SMTP('smtp.eu.mailgun.org', 587) # (1) smtp_object.starttls() # (2) smtp_object.login( # (3) config('EMAIL_HOST_USER'), config('EMAIL_HOST_PASSWORD') ) smtp_object.sendmail( # (4) config('TESTS_REPORT_SENDER_ADDRESS'), self.__EMAILS, self.__report.as_string() ) smtp_object.quit() # (5)
Свойство __status
Перейдём к свойствам класса TestLauncher. Первое — __status. Оно в зависимости от значения статус-кода процесса возвращает текст, который будет добавлен в отчёт.
@property def __status(self) -> str: """Статус выполнения тестов""" return ( self.__SUCCESS_STATUS if self.__is_successful() else self.__FAILURE_STATUS )
Свойство __body
__body возвращает полный текст для отчёта с MIME-типом text.
@property def __body(self) -> str: """Полное содержимое отчёта о выполнении тестов""" body = f'
'.replace('\n', '
') return MIMEText(body, 'html')
Свойство __report
Последнее свойство — __report. Как бы это ни было удивительно, но оно возвращает отчёт о выполнении тестов с MIME-типом multipart. Сначала указывается тема, после с помощью метода attach добавляется основное содержимое.
@property def __report(self) -> MIMEMultipart: """Отчёт о выполнении тестов""" report = MIMEMultipart() report['Subject'] = self.__REPORT_SUBJECT report.attach(self.__body) return report
Отправка сообщения
Заключительная часть этого кода — создание объекта класса TestLauncher и отправка отчёта.
if __name__ == '__main__': test_launcher = TestLauncher() test_launcher.send_report()
Запуск скрипта
В кроне пропишем следующую команду:
@daily /usr/bin/python3 send_tests_report.py
Она ежедневно в 00:00 (для Timezone – UTC) будет выполнять созданный нами скрипт.
При его успешном выполнении на почту придёт такое сообщение:
При фейле сообщение будет таким:
Заключение
Тестирование кода – это хорошо, но не стоит забывать про безопасность. С этой точки зрения интерес может представлять методология статического анализа кода для выявления потенциальных уязвимостей в back-end части. Вот несколько статей, которые познакомят вас с этой темой:
- Почему моё приложение при открытии SVG-файла отправляет сетевые запросы?
- XSS: атака и защита с точки зрения C# программирования.
- OWASP, уязвимости и taint анализ в PVS-Studio C#. Смешать, но не взбалтывать.
Надеюсь, эта статья была вам полезна. Для фидбека или критики пишите в комментарии, либо в мой инстаграм. Спасибо за внимание и до скорых встреч)
- Блог компании PVS-Studio
- Тестирование IT-систем
- Python
- Django
- Тестирование веб-сервисов
Написание и запуск тестов ¶
Этот документ разделен на два основных раздела. Сначала мы объясним, как писать тесты с помощью Django. Затем мы объясняем, как их выполнять.
Написание тестов ¶
Модульные тесты Django используют unittest стандартный модуль библиотеки Python. Этот модуль определяет тесты в соответствии с подходом на основе классов.
Вот пример класса, наследующего django.test.TestCase , наследующего unittest.TestCase и выполняющего каждый тест в транзакции для обеспечения изоляции:
from django.test import TestCase from myapp.models import Animal class AnimalTestCase(TestCase): def setUp(self): Animal.objects.create(name="lion", sound="roar") Animal.objects.create(name="cat", sound="meow") def test_animals_can_speak(self): """Animals that can speak are correctly identified""" lion = Animal.objects.get(name="lion") cat = Animal.objects.get(name="cat") self.assertEqual(lion.speak(), 'The lion says "roar"') self.assertEqual(cat.speak(), 'The cat says "meow"')
Когда вы запускаете тесты , по умолчанию тестовая утилита находит все тестовые примеры (то есть подклассы unittest.TestCase ) во всех файлах с именами, начинающимися с test , затем автоматически создать набор тестов и запустить его.
Дополнительные сведения unittest см. В документации Python.
Где должны быть тесты?
Шаблон по умолчанию startapp создает файл tests.py в новом приложении. Это хорошо работает, когда есть всего несколько тестов. Но как только набор тестов растет становится лучше реструктурировать файл в модуле Python для того , чтобы отделить тестирование суб-модули , такие как test_models.py , test_views.py , test_forms.py и т.д. Вы можете выбрать наиболее подходящее структурное подразделение.
Если тесты зависят от доступа к базе данных для создания или запроса моделей, тестовые классы должны быть подклассами, django.test.TestCase а не unittest.TestCase .
Используя unittest.TestCase , мы упрощаем тесты, избегая этапа обертывания каждого теста в транзакции и повторной инициализации базы данных. Но если затронутые тесты взаимодействуют с базой данных, их поведение будет варьироваться в зависимости от порядка, в котором они запускаются исполнителем тестов. Это может привести к тому, что некоторые модульные тесты будут выполняться при индивидуальном запуске, но не пройдут, если они являются частью набора.
Запуск тестов ¶
Когда тесты написаны, встает вопрос о их запуске командой test утилиты manage.py проекта:
$ ./manage.py test
Открытие тестов на основе модуля unittest с интегрированным открытия тестов . По умолчанию тесты ищутся в любом файле с именем «test * .py» во всей древовидной структуре из текущего рабочего каталога.
Вы можете выбрать конкретные тесты для запуска, указав «имена тестов» при заказе . Каждое имя теста может быть полным путем Python в указанном синтаксисе к пакету, модулю, подклассу или методу тестирования. Например : ./manage.py test TestCase
# Run all the tests in the animals.tests module $ ./manage.py test animals.tests # Run all the tests found within the 'animals' package $ ./manage.py test animals # Run just one test case $ ./manage.py test animals.tests.AnimalTestCase # Run just one test method $ ./manage.py test animals.tests.AnimalTestCase.test_animals_can_speak
Также можно указать путь к каталогу для поиска всех тестов в древовидной структуре этого каталога:
$ ./manage.py test animals/
Вы можете указать собственный шаблон имени файла с помощью параметра -p (или —pattern ), если ваши тестовые файлы не соответствуют шаблону test*.py :
$ ./manage.py test --pattern="tests_*.py"
Если вы нажмете Ctrl-C во время выполнения тестов, средство запуска тестов дождется завершения текущего выполняемого теста, а затем плавно выйдет из тестов. При выходе из тестов таким образом средство запуска отображает подробную информацию о сбоях теста, указывает, сколько тестов было выполнено и количество обнаруженных ошибок и сбоев, а затем он удаляет тестовые базы данных как он обычно делает. Таким образом, это Ctrl-C может быть очень полезно, если вы забыли передать опцию —failfast , понимаете, что некоторые тесты неожиданно завершаются неудачно, и вы хотите получить подробную информацию об этих сбоях, не дожидаясь завершения всего набора тестов.
Если вы не хотите ждать окончания текущего теста, вы можете нажать второй раз, Ctrl-C и тест завершится немедленно, но внезапно. Вы не увидите никаких подробностей о тестах, запущенных до этого момента, и все тестовые базы данных, созданные для этого запуска, не будут удалены.
Тесты с активными предупреждениями
Желательно запускать тесты, активировав предупреждения Python . Флаг указывает Python отображать предупреждения об устаревании. Django, как и любая другая библиотека Python, использует эти предупреждения для уведомления о том, что некоторые функции скоро исчезнут. Есть также предостережения, которые могут указать на то, что некоторые области кода не являются в корне неправильными, но могут выиграть от лучшей реализации. python -Wa manage.py test -Wa
Тестовая база данных ¶
Тесты, которым нужна база данных (т.е. тесты с моделями), не используют «настоящую» (производственную) базу данных. Отдельные пустые базы данных создаются специально для тестирования.
Независимо от того, пройдены ли тесты или нет, тестовые базы данных удаляются сразу после выполнения всех тестов.
Вы можете избежать отбрасывания тестовых баз данных, используя опцию . Это помогает сохранять тестовую базу данных между сессиями. Если база данных не существует, она все равно будет создана. Любая миграция также будет применяться, чтобы поддерживать базу данных в актуальном состоянии. test —keepdb
Как объяснялось в предыдущем разделе, если тестовый запуск внезапно прерывается, тестовая база данных не может быть уничтожена. При следующем запуске вас спросят, хотите ли вы повторно использовать или уничтожить базу данных. Используйте опцию, чтобы удалить этот вопрос и автоматически уничтожить базу данных. Это может быть полезно при запуске тестов на сервере непрерывной интеграции, где тесты могут быть остановлены, например, по таймауту. test —noinput
Имена баз данных по умолчанию испытаний создаются предваряя значения каждого NAME в DATABASES с test_ . В SQLite тесты по умолчанию используют базу данных в памяти (то есть база данных создается в памяти без использования файловой системы). Словарь TEST в DATABASES предложениях нескольких параметров , чтобы настроить тестовую базу данных. Например, если вы хотите использовать другое имя базы данных, заполните NAME словарь TEST затронутой базы данных DATABASES .
В PostgreSQL USER также требуется доступ для чтения к встроенной базе данных postgres .
Кроме того, создание отдельной базы данных, тест бегун использует одни и те же параметры базы данных из файла настроек: ENGINE , USER , и HOST т.д. Тестовая база данных создается пользователем USER , поэтому вы должны убедиться, что эта учетная запись пользователя имеет необходимые права для создания новой базы данных в вашей системе.
Для более точного управления кодировкой символов в тестовой базе данных используйте параметр CHARSET TEST. С MySQL также можно использовать параметр COLLATION для управления сопоставлением, используемым тестовой базой данных. См. Документацию по настройкам для получения более подробной информации об этих расширенных настройках.
Если вы используете базу данных SQLite в памяти, общий кеш включен, чтобы вы могли писать тесты с возможностью совместного использования базы данных между потоками.
Доступ к данным производственной базы данных во время выполнения тестов?
Если ваш код пытается получить доступ к базе данных во время компиляции кода, это произойдет до того, как база данных будет настроена, с потенциально неожиданными результатами. Например, если запрос к базе данных запускается в коде уровня модуля и существует фактическая база данных, производственные данные могут помешать тестированию. В любом случае настоятельно рекомендуется не размещать запросы к базе данных, выполняемые при импорте кода , это вопрос переписывания кода, чтобы этого не произошло.
Это также относится к пользовательским реализациям ready() .
Порядок выполнения тестов ¶
Чтобы гарантировать, что весь код TestCase начинается с чистой базы данных, средство запуска тестов Django переупорядочивает тесты следующим образом:
- Все подклассы TestCase выполняются первыми.
- Затем все другие тесты, основанные на Django ( SimpleTestCase включая классы тестов на основе случаев TransactionTestCase ), выполняются без гарантированного определенного порядка сортировки.
- Затем unittest.TestCase запускаются все остальные тесты (включая «doctests»), которые могут изменить базу данных без восстановления ее в начальное состояние.
Новый порядок выполнения тестов может выявить неожиданные зависимости в порядке тестовых примеров. Так обстоит дело с «doctests», которые полагаются на состояние базы данных после TransactionTestCase данного теста ; эти тесты необходимо обновить, чтобы они могли функционировать независимо.
Вы можете изменить порядок выполнения внутри групп, передав эту опцию . Это может помочь гарантировать независимость тестов друг от друга. test —reverse
Эмуляция отката ¶
Любые исходные данные, загруженные при миграциях, доступны только в тестах TestCase , но не в тестах TransactionTestCase и, более того, только с движками, поддерживающими транзакции (наиболее заметным исключением является MyISAM). Это также верно для тестов, основанных на TransactionTestCase таких как LiveServerTestCase и StaticLiveServerTestCase .
Django может перезагрузить эти данные для вас для каждого теста , если вы установите опцию , serialized_rollback чтобы True в теле TestCase или TransactionTestCase , но имейте в виду , что это приведет к замедлению пораженный тестов примерно в 3 раза.
Сторонние приложения или приложения, основанные на механизме MyISAM, должны устанавливать этот атрибут. Однако обычно рекомендуется разрабатывать свои проекты с использованием транзакционной базы данных и тестировать с помощью тестового класса TestCase , и в этом случае этот атрибут не нужен.
Первоначальная сериализация обычно выполняется очень быстро, но если вы хотите исключить определенные приложения из этого процесса (и немного ускорить выполнение теста), вы можете добавить эти приложения в настройку TEST_NON_SERIALIZED_APPS .
Чтобы сериализованные данные не загружались дважды, можно настроить serialized_rollback=True отключение сигнала post_migrate при сбросе тестовой базы данных.
Другие условия испытаний ¶
Все тесты Django запускаются с DEBUG = False, независимо от значения параметра DEBUG в вашем файле настроек. Это гарантирует, что наблюдаемый результат кода соответствует тому, что фактически будет производиться в производственной среде.
Кеши не очищаются после каждого теста, и запуск «manage.py test fooapp» может вставить тестовые данные в кеш онлайн-системы, если вы запускаете тесты в производственной среде, потому что, в отличие от того, что делается с базами данных тесты не используют «тестовый кеш». Это поведение может измениться в будущем.
Что такое тестовый дисплей ¶
Когда вы запускаете тесты, по мере подготовки исполнителя тестов появляется ряд сообщений. Вы можете контролировать уровень детализации этих сообщений с помощью параметра verbosity командной строки:
Creating test database. Creating table myapp_animal Creating table myapp_mineral
Средство выполнения тестов сообщает вам, что он создает тестовую базу данных, как описано в предыдущем разделе.
После создания тестовой базы данных Django запускает фактические тесты. Если все пойдет хорошо, вы увидите что-то вроде этого:
---------------------------------------------------------------------- Ran 22 tests in 0.221s OK
Однако, если какие-либо тесты не прошли, вы увидите полную информацию о неудачных тестах:
====================================================================== FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/dev/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll self.assertIs(future_poll.was_published_recently(), False) AssertionError: True is not False ---------------------------------------------------------------------- Ran 1 test in 0.003s FAILED (failures=1)
Подробное объяснение этого отображения ошибок не входит в задачу этого документа, но это относительно интуитивно понятно. Вы можете обратиться к документации библиотеки Python unittest для получения более подробной информации.
Обратите внимание, что код возврата сценария запуска теста равен 1, независимо от количества неудачных или ошибочных тестов. Если все тесты пройдены, код возврата равен 0. Эта функция полезна, если вы запускаете сценарий тестового запуска из сценария оболочки и вам нужно проверить результат (прошел или не прошел) на этом уровне. ,
Ускорение тестирования ¶
Параллельное выполнение тестов ¶
Пока ваши тесты полностью изолированы, их можно запускать параллельно, чтобы сэкономить время на многоядерном оборудовании. Смотрите . test —parallel
Хеширование паролей ¶
Хеширование пароля по умолчанию намеренно выполняется довольно медленно. Если ваши тесты проверяют подлинность большого количества пользователей, рекомендуется создать файл настроек для конкретного теста и установить PASSWORD_HASHERS для него более быстрый алгоритм хеширования:
PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.MD5PasswordHasher', ]
Не забудьте также включить в PASSWORD_HASHERS любые хэш-алгоритмы, используемые в моментальных снимках, если применимо.
Сохранение тестовой базы ¶
Эта опция сохраняет тестовую базу данных между несколькими запусками. Это игнорирует действия create и destroy и может значительно сократить время выполнения теста. test —keepdb
Как покрыть приложение на Django модульными тестами
Примечание: чтобы использовать это руководство, вам понадобятся навыки, приобретенные после прочтения Как создать API с помощью Python и Django, а также код, над которым мы работали тогда. Мы продолжим работу с тем же API для приложения со списком дел, и будем использовать вот эту версию на GitHub.
Прошлым летом я работал над веб-приложением на Django со своими друзьями. Как-то раз я создал одну функцию и отправил своему другу на тестирование. Он ответил, что не смог ее протестировать, поскольку я случайно сломал функцию входа в систему. Однако это был не единственный случай, что-то подобное происходило каждую неделю. Основная проблема заключалась в том, что мы не использовали полноценное автоматизированное тестирование в нашем рабочем процессе.
Что такое автоматизированное тестирование?
Тестирование кода — одна из важнейших частей разработки. Тестирование может быть разным: можно протестировать приложение, всего лишь взглянув на веб-страницу, поиграв в видеоигру или проанализировав логи. Все зависит от типа вашего проекта. Ручное тестирование отнимает много времени и допускает возможность ошибок, поэтому профессиональные разработчики стараются уделять больше внимания автоматизированному тестированию. В этой статье мы рассмотрим общий вид автоматизированного тестирования, модульные тесты, а также поговорим о том, как автоматизированное тестирование может помочь нам в процессе разработки. Мы начнем с написания тестов для моделей и представлений (views) существующего API, затем, добавив новую функцию, попрактикуемся в разработке через тестирование.
Автоматизированное тестирование экономит время и делает программное обеспечение качественней. Поначалу ручное тестирование кажется быстрым: нужно просто запустить код и посмотреть, работает ли он. Однако со временем, чем больше и больше функций вы добавляете в свое приложение, тем больше времени начинает отнимать такой тип тестирования. К тому же вы можете просто забыть протестировать определенные вещи. Правильно реализованное автоматизированное тестирование охватывает все, работает всегда и занимает считанные секунды. Оно также делает код проще для понимания, что позволяет одновременно нескольким командам работать над кодом, не беспокоясь о том, что они могут сломать чью-то функцию.
Модульный тест по отдельности проверяет функциональность компонентов. Это тестирование самого низкого уровня, оно проверяет, что каждый компонент программы правильно работает в одиночку (интеграционные и системные тесты проверяют все компоненты вместе, а также их взаимодействия, но эта тема выходит за рамки данного руководства). При объектно-ориентированном подходе, например, вам пришлось бы писать модульные тесты для каждого объекта, а также для отдельных методов, в зависимости от их сложности. В Django мы используем модульное тестирование для каждой модели и представления.
Как в Django работает модульное тестирование?
Для работы с этим руководством, клонируйте вот этот проект из GitHub. Чтобы создать проект, выполните действия из первых трех параграфов в Как создать API с помощью Python и Django.
Когда вы используете python manage.py startapp appname для создания приложения на Django, один из создаваемых Django файлов папке appname имеет имя tests.py . Этот файл существует для размещения модульных тестов для моделей и других компонентов внутри приложения. По умолчанию этот файл содержит одну строчку кода: from django.test import TestCase . Test case содержит несколько связанных тестов для одного и того же фрагмента кода. TestCase — это объект Django, который мы будем наследовать для создания собственных модульных тестов. У класса есть два метода: setUp(self) и tearDown(self) , которые запускаются до и после отдельных тестовых функций для того, чтобы предоставить и очистить тестовую базу данных. Эта база независима от той базы данных, к которой вы получаете доступ с помощью python manage.py runserver . Чтобы взглянуть на код, откройте tests.py в папке todo нашего проекта.
class SigninTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user(username='test', password='12test12', email='test@example.com') self.user.save() def tearDown(self): self.user.delete() def test_correct(self): user = authenticate(username='test', password='12test12') self.assertTrue((user is not None) and user.is_authenticated) def test_wrong_username(self): user = authenticate(username='wrong', password='12test12') self.assertFalse(user is not None and user.is_authenticated) def test_wrong_pssword(self): user = authenticate(username='test', password='wrong') self.assertFalse(user is not None and user.is_authenticated)
Здесь мы тестируем функцию входа в систему. Поскольку мы используем встроенные в Django методы, все должно работать нормально, если в базе данных нет никаких проблем. Нам нужны только простые тесты: аутентифицируйтесь, если предоставлены верные данные, и не делайте этого, если нет. Благодаря этому примеру мы можем увидеть кое-что еще в модульном тестировании в Django. Прежде всего, все тестовые методы в тестовом случае должны начинаться с test_ , чтобы быть выполненными при запуске тестовой команды python manage.py test . Остальные методы в тестовом случае нужно воспринимать как вспомогательные функции. Также необходимо знать, что все тестовые методы должны принимать self в качестве аргумента, где self является ссылкой на объект TestCase . Класс TestCase , который мы наследуем для создания нашего класса, содержит методы утверждений для проверки логических значений. Вызов self.assertSomething() проходит, если переданные в качестве аргументов значения соответствуют утверждению, в противном случае этого не происходит. Тестовый метод проходит, только если каждое утверждение в методе проходит.
class TaskTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user(username='test', password='12test12', email='test@example.com') self.user.save() self.timestamp = date.today() self.task = Task(user=self.user, description='description', due=self.timestamp + timedelta(days=1)) self.task.save() def tearDown(self): self.user.delete() def test_read_task(self): self.assertEqual(self.task.user, self.user) self.assertEqual(self.task.description, 'description') self.assertEqual(self.task.due, self.timestamp + timedelta(days=1)) def test_update_task_description(self): self.task.description = 'new description' self.task.save() self.assertEqual(self.task.description, 'new description') def test_update_task_due(self): self.task.due = self.timestamp + timedelta(days=2) self.task.save() self.assertEqual(self.task.due, self.timestamp + timedelta(days=2))
Теперь давайте протестируем нашу модель: объект Task , определенный в models.py . Для тестового случая мы создаем пользователя и задачу (обратите внимание, что из-за того, что пользователь и задача связаны отношениями внешнего ключа, удаление пользователя в tearDown() приведет к удалению задачи). Здесь мы можем увидеть, что любой тестовый метод может иметь несколько утверждений и проходит только в том случае, если все они выполняются успешно. Когда мы обновляем задачу, мы можем записывать данные в базу вне функции setUp. В остальном, этот тест похож на тест функции входа. Большинство тестовых случаев для моделей представляют собой создание, чтение, модифицирование и удаление объектов в базе данных, хотя модели с методами и интереснее тестировать.
class SignInViewTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user(username='test', password='12test12', email='test@example.com') def tearDown(self): self.user.delete() def test_correct(self): response = self.client.post('/signin/', 'username': 'test', 'password': '12test12'>) self.assertTrue(response.data['authenticated']) def test_wrong_username(self): response = self.client.post('/signin/', 'username': 'wrong', 'password': '12test12'>) self.assertFalse(response.data['authenticated']) def test_wrong_pssword(self): response = self.client.post('/signin/', 'username': 'test', 'password': 'wrong'>) self.assertFalse(response.data['authenticated'])
Тестировать представления несколько сложнее, чем модели. Однако поскольку мы пишем API, в отличие от веб-приложения, здесь можно не волноваться по поводу тестирования фронтенда. Большую часть ручных тестов посредством Postman можно заменить на тесты представлений. self.client — HTTP-клиент тестовой библиотеки Django. Мы используем его для создания post-запроса к «/signin/» с учетными данными пользователя. Мы тестируем то же, что и раньше: верные учетные данные, неправильное имя пользователя и неправильный пароль. Это очень полезно, так как мы видим, что если тесты модели не выявляют ошибок, а тесты представлений выявляют — проблема не в модели, что в свою очередь позволяет тратить меньше времени на устранение багов. Мы делаем примерно то же самое для представлений, связанных с задачами.
class AllTasksViewTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user(username='test', password='12test12', email='test@example.com') self.user.save() self.timestamp = date.today() self.client.login(username='test', password='12test12') def tearDown(self): self.user.delete() def test_no_tasks(self): response = self.client.get('/all/') self.assertEqual(response.data, 'tasks': []>) def test_one_task(self): self.task1 = Task(user=self.user, description='description 1', due=self.timestamp + timedelta(days=1)) self.task1.save() response = self.client.get('/all/') self.assertEqual(response.data, 'tasks': [OrderedDict([('id', 1), ('description', 'description 1'), ('due', str(self.timestamp + timedelta(days=1)))])]>)
Этот случай тестирует конечную точку «/all/». На самом деле у этого теста больше методов, но фрагмент выше показывает только новое. Чтобы клиент мог действовать как вошедший в систему пользователь, в setUp мы используем self.client.login() . Затем мы создаем задачи и сравниваем их с ожидаемым отформатированным выводом. Этот пример хорошо иллюстрирует преимущества методов setUp() и tearDown() , так как задачи из одного теста не переносятся в другие. Опять же, этот тест изолирует компонент представления, поскольку базовая модель тестируется отдельно.
Когда разберетесь с тестовым кодом, запустите python manage.py test , чтобы выполнить все тесты. Давайте взглянем на результат:
Creating test database for alias 'default'. System check identified no issues (0 silenced). . FF. ====================================================================== FAIL: test_due_future (todo.tests.DueTodayTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/Philip/Code/WFH/mkdev_blog/djangotesting/taskmanager/todo/tests.py", line 155, in test_due_future self.assertFalse(self.task.due_today()) AssertionError: True is not false ====================================================================== FAIL: test_due_past (todo.tests.DueTodayTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/Philip/Code/WFH/mkdev_blog/djangotesting/taskmanager/todo/tests.py", line 161, in test_due_past self.assertFalse(self.task.due_today()) AssertionError: True is not false ---------------------------------------------------------------------- Ran 15 tests in 2.232s FAILED (failures=2) Destroying test database for alias 'default'.
Все тесты, не выявившие ошибок, помечаются . , а тесты, показавшие ошибки — F . Такие тесты также показывают, почему именно утверждения не прошли. Мы еще не говорили с вами о тех тестах, которые выявили ошибки, но мы исправимся чуть ниже. Вы могли заметить, что код теста очень подробен. Конечно же, мы протестировали лишь небольшую часть нашего функционала, но, несмотря на это, уже получили столько же строк кода, сколько и в файле представления. Этого и следовало ожидать. Если вы хотите, чтобы ваши тесты были точными, вы не можете проводить их слишком часто. При изменениях в коде некоторые тесты перестанут работать. Таким образом, вы сможете понять, какие ошибки и в каких тестах нужно будет устранить. Так что будьте готовы к тому, что код тестов будет все расти и расти, а в среднестатистическом приложении будет столько же строк тестового кода, сколько и у кода самого приложения.
Что такое разработка через тестирование?
Давайте на секундочку вернемся к рассказу из начала статьи. Наша команда неделя за неделей сражалась с багами и в результате написала модульные тесты для всей базы данных. Мы покрыли тестами абсолютно все, каждая строчка кода проверялась по меньшей мере одним тестом. Мы поработали так пару недель, пока не решили существенно изменить структуру нашей базы данных. Вместо того чтобы переписывать тесты, мы стали отбрасывать неработающие и буквально через несколько дней у нас стали снова вылезать случайные поломки. Разработка через тестирование помогла бы это предотвратить.
Чтобы тесты оставались актуальными, их нужно обновлять по мере обновления кода. Некоторые разработчики пользуются разработкой на основе тестов, чтобы всегда быть готовыми к любым изменениям в коде. Первое, что вы делаете при разработке функции — определяете что, собственно, эта функция будет делать. Разработка через тестирование формализует этот процесс, поскольку при таком подходе вы, прежде всего, прописываете тесты для этой самой функциональности. Основная идея заключается в том, что вы пишите один или несколько тестов, которые определяют функцию, переписываете код до тех пор, пока тесты не выявят ошибок, а затем снова пишите еще больше тестов. Вернемся к тестам, выявившим ошибки. Нам нужно написать метод due_today() в модели Task . Согласно тесту, этот метод должен возвращать True , если задача должна быть выполнена сегодня и False , если нет. Скопируйте код ниже для замены существующего метода due_today() в модели Task , а затем запустите тесты снова. python def due_today(self): return self.due == date.today()
Тест не показывает ошибок, что значит, что наша функция работает и можно продолжать. Подобный подход к разработке требует больших физических и умственных усилий поначалу для определения поведения кода, но в результате значительно упрощает сам процесс разработки.
Чтобы протестировать разобрались ли вы, попробуйте написать тесты для остальных представлений или используйте тесты для задания новых функций, а затем напишите эти функции. Одним из простых вариантов будет поле с логическим значением completed в модели task , которому может быть присвоено значение True как только задача будет выполнена. Это позволит нам не удалять выполненные задачи, а оставить их. Затем, подумайте о том, чтобы добавить тесты в ваши личные проекты. Да, вас может напугать перспектива покрытия тестами огромного проекта, который ранее не тестировался. Вместо того, чтобы пытаться протестировать все и сразу, попробуйте добавить тесты в маленькие фрагменты проекта или новые функции непосредственно во время разработки до полного покрытия.
Материалы для ознакомления:
- Официальное руководство Django, часть 5
- Обзор документации Django по тестированию
- Раздел по тестированию для продвинутых пользователей. Включает в себя покрытие тестами
- Документация Django по тестированию кода Django
© Copyright 2014 — 2024 mkdev | Privacy Policy