Перейти к содержимому

Чем отличается unittest в django от стандартной python библиотеки unittest

  • автор:

Тестируем на Python: unittest и pytest. Инструкция для начинающих

Python-разработчик Андрей Смирнов рассказал, как написать первые тесты и какие фреймворки выбрать: unittest или pytest.

Меня зовут Андрей Смирнов, я занимаюсь Python-разработкой, автоматизацией технических процессов и преподаю промышленное программирование в Школе программистов МШП.

Не секрет, что разработчики создают программы, которые рано или поздно становятся очень масштабными (если смотреть на количество строчек кода). А с этим приходит и большая ответственность за качество.

Сейчас расскажу, как unittest и pytest помогут найти ошибки в программах и исключить их в будущем.

Итак, тестирование

Каждый, кто писал первые программы (будь то классический «hello, world» или же калькулятор), всегда запускал тесты, чтобы проверить их работу.

Сам факт запуска — самое первое, незримое касание технологии тестирования в вашей жизни. Рассмотрим его как процесс поиска ошибок на чуть более сложной программе.

Например, вам нужно ввести три числа (a, b, c) и найти корни квадратного уравнения. Для решения пишем код:

from math import sqrt def square_eq_solver(a, b, c): result = [] discriminant = b * b - 4 * a * c if discriminant == 0: result.append(-b / (2 * a)) else: result.append((-b + sqrt(discriminant)) / (2 * a)) result.append((-b - sqrt(discriminant)) / (2 * a)) return result def show_result(data): if len(data) > 0: for index, value in enumerate(data): print(f'Корень номер равен ') else: print('Уравнение с заданными параметрами не имеет корней') def main(): a, b, c = map(int, input('Пожалуйста, введите три числа через пробел: ').split()) result = square_eq_solver(a, b, c) show_result(result) if __name__ == '__main__': main() 

Сразу оговорюсь: любую задачу, какой бы она ни была краткой, я рассматриваю с позиции «когда-нибудь она вырастет и станет очень объёмной». Поэтому всегда стараюсь разделять программу на различные подпрограммы (ввод/обработка/вывод).

Возможно, вы уже заметили ошибку в коде. Однако иногда она может быть скрыта настолько глубоко, что её просто так не обнаружишь. И в таком случае единственный способ вывести ее на свет — протестировать код. Как это сделать?

— зная алгоритм нахождения корней уравнения, определяем наборы входных данных, которые будут переданы на вход программе;

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

— запускаем программу и передаем ей на вход исходные данные;

— получаем от нее ответ и сравниваем с тем, который должен быть получен. Если они совпадают — хорошо, идём к следующему набору данных, если нет, сообщаем об ошибке.

Например, для данной задачи можно подобрать следующие тесты:

  • 10x**2 = 0 — единственный корень x=0
  • 2x**2 + 5x — 3 = 0 — у такого уравнения два корня (x1 = 0.5, x2=-3)
  • 10x**2+2 = 0 — у этого уравнения корней нет

Тесты подобрали, что дальше? Правильно, запускаем:

Тест номер 1 > python.exe example.py Пожалуйста, введите три числа через пробел: 10 0 0 Корень номер 0 равен 0.00 Тест номер 2: > python.exe example.py Пожалуйста, введите три числа через пробел: 2 5 -3 Корень номер 1 равен 0.50 Корень номер 2 равен -3.00 Тест номер 3: > python.exe example.py Пожалуйста, введите три числа через пробел: 10 0 2 Traceback (most recent call last): File "C:PyProjectstprogerexample.py", line 32, in main() File "C:PyProjectstprogerexample.py", line 27, in main result = square_eq_solver(a, b, c) File "C:PyProjectstprogerexample.py", line 11, in square_eq_solver result.append((-b + sqrt(discriminant)) / (2 * a)) ValueError: math domain error 

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

def square_eq_solver(a, b, c): result = [] discriminant = b * b - 4 * a * c if discriminant == 0: result.append(-b / (2 * a)) elif discriminant > 0: # 

Запускаем все тесты повторно и они срабатывают нормально.

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

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

Что делает Python-разработчик в облаке

Unittest и pytest: пишем тесты

Две самые популярные библиотеки — unittest и pytest. Попробуем каждую, чтобы объективно оценить синтаксис.

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

Формат кода

По формату написания тестов она сильно напоминает библиотеку JUnit, используемую в языке Java для написания тестов:

  • тесты должны быть написаны в классе;
  • класс должен быть отнаследован от базового класса unittest.TestCase;
  • имена всех функций, являющихся тестами, должны начинаться с ключевого слова test;
  • внутри функций должны быть вызовы операторов сравнения (assertX) — именно они будут проверять наши полученные значения на соответствие заявленным.

Пример использования unittest для нашей задачи

import unittest class SquareEqSolverTestCase(unittest.TestCase): def test_no_root(self): res = square_eq_solver(10, 0, 2) self.assertEqual(len(res), 0) def test_single_root(self): res = square_eq_solver(10, 0, 0) self.assertEqual(len(res), 1) self.assertEqual(res, [0]) def test_multiple_root(self): res = square_eq_solver(2, 5, -3) self.assertEqual(len(res), 2) self.assertEqual(res, [0.5, -3]) 

Запускается данный код следующей командой

python.exe -m unittest example.py

И в результате на экран будет выведено:

> python.exe -m unittest example.py . ------------------------------------------------------------------ Ran 3 tests in 0.001s OK 

В случае, если в каком-нибудь из тестов будет обнаружена ошибка, unittest не замедлит о ней сообщить:

> python.exe -m unittest example.py F.. ================================================================== FAIL: test_multiple_root (hello.SquareEqSolverTestCase) ------------------------------------------------------------------ Traceback (most recent call last): File "C:PyProjectstprogerexample.py", line 101, in test_multiple_root self.assertEqual(len(res), 3) AssertionError: 2 != 3 ------------------------------------------------------------------ Ran 3 tests in 0.001s FAILED (failures=1) 

Unittest: аргументы “за”

  • Является частью стандартной библиотеки языка Python: не нужно устанавливать ничего дополнительно;
  • Гибкая структура и условия запуска тестов. Для каждого теста можно назначить теги, в соответствии с которыми будем запускаться либо одна, либо другая группа тестов;
  • Быстрая генерация отчетов о проведенном тестировании, как в формате plaintext, так и в формате XML.

Unittest: аргументы “против”

  • Для проведения тестирования придётся написать достаточно большое количество кода (по сравнению с другими библиотеками);
  • Из-за того, что разработчики вдохновлялись форматом библиотеки JUnit, названия основных функций написаны в стиле camelCase (например setUp и assertEqual);
  • В языке python согласно рекомендациям pep8 должен использоваться формат названий snake_case (например set_up и assert_equal).

Pytest

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

Pytest позволяет провести модульное тестирование (тестирование отдельных компонентов программы), функциональное тестирование (тестирование способности кода удовлетворять бизнес-требования), тестирование API (application programming interface) и многое другое.

Формат кода

Написание тестов здесь намного проще, нежели в unittest. Вам нужно просто написать несколько функций, удовлетворяющих следующим условиям:

  • Название функции должно начинаться с ключевого слова test;
  • Внутри функции должно проверяться логическое выражение при помощи оператора assert.

Пример использования pytest для нашей задачи:

def test_no_root(): res = square_eq_solver(10, 0, 2) assert len(res) == 0 def test_single_root(): res = square_eq_solver(10, 0, 0) assert len(res) == 1 assert res == [0] def test_multiple_root(): res = square_eq_solver(2, 5, -3) assert len(res) == 3 assert res == [0.5, -3] 

Запускается данный код следующей командой

pytest.exe example.py

И в результате на экран будет выведено:

> pytest.exe example.py ======================= test session starts ====================== platform win32 -- Python 3.9.6, pytest-7.1.2, pluggy-1.0.0 rootdir: C:PyProjectstproger collected 3 items example.py . [100%] ======================== 3 passed in 0.03s ======================= 

В случае ошибки вывод будет несколько больше:

> pytest.exe example.py ======================= test session starts ====================== platform win32 -- Python 3.9.6, pytest-7.1.2, pluggy-1.0.0 rootdir: C:PyProjectstproger collected 3 items example.py ..F [100%] ============================ FAILURES ============================ _______________________ test_multiple_root _______________________ def test_multiple_root(): res = square_eq_solver(2, 5, -3) > assert len(res) == 3 E assert 2 == 3 E + where 2 = len([0.5, -3.0]) example.py:116: AssertionError ===================== short test summary info ==================== FAILED example.py::test_multiple_root - assert 2 == 3 =================== 1 failed, 2 passed in 0.10s ================== 

Pytest: аргументы “за”

  • Позволяет писать компактные (по сравнению с unittest) наборы тестов;
  • В случае возникновения ошибок выводится гораздо больше информации о них;
  • Позволяет запускать тесты, написанные для других тестирующих систем;
  • Имеет систему плагинов (и сотни этих самых плагинов), расширяющую возможности фреймворка. Примеры таких плагинов: pytest-cov, pytest-django, pytest-bdd;
  • Позволяет запускать тесты в параллели (при помощи плагина pytest-xdist).

Pytest: аргументы “против”

  • pytest не входит в стандартную библиотеку языка Python. Поэтому его придётся устанавливать отдельно при помощи команды pip install pytest;
  • совместимость кода с другими фреймворками отсутствует. Так что, если напишете код под pytest, запустить его при помощи встроенного unittest не получится.

Ну и что лучше?

  • Если вам нужно базовое юнит-тестирование и вы знакомы с фреймворками вида xUnit, тогда вам подойдёт unittest.
  • Если нужен фреймворк, позволяющий создавать краткие и изящные тесты, реализующие сложную логику проверок, то pytest.

Post Scriptum

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

Кстати, в этой программе я намеренно оставил ещё одну ошибку (на сей раз уже логическую), связанную с нахождением корня. Напишите в комментариях, с чем она может быть связана, и какой тест поможет её отловить ?

Руководство часть 10: Тестирование приложений Django

Сайты, в процессе развития и разработки, становится все сложнее тестировать вручную. Кроме такого тестирования, сложными становятся внутренние взаимодействия между компонентами - внесение небольшого изменения в одной части приложения влияет на другие. При этом, чтобы все продолжало работать нужно вносить все больше и больше изменений и, желательно так, чтобы не добавлялись новые ошибки. Одним из способов который позволяет смягчить последствия добавления изменений, является внедрение в разработку автоматического тестирования - оно должно просто и надёжно запускаться каждый раз, когда вы вносите изменения в свой код. Данное руководство рассматривает вопросы автоматизации юнит-тестирования вашего сайта при помощи фреймворка Django для тестов.

Требования: Изучить все предыдущие темы руководства, включая Руководство Django Часть 9: Работа с формами.
Цель: Понимать как создавать юнит тесты для сайта на основе Django.

Обзор

LocalLibrary в настоящий момент содержит страницы для показа списков всех книг, авторов, подробной информации о книгах Book и авторах Author , а также страницу для обновления информации об экземпляре книги BookInstance и, кроме того, страницы для создания, обновления и удаления записей модели Author (и модели Book , в том случае, если вы выполнили домашнее задание в руководстве работа с формами). Даже в случае небольшого сайта, ручной переход на каждую страницу и беглая проверка того, что все работает как следует, может занять несколько минут. В процессе внесения изменений и роста сайта требуемое время для проведения проверок будет только возрастать. Если бы мы продолжили в том же духе, то в какой-то момент на проведение тестов мы тратили бы больше времени, чем на написание кода и внесение изменений.

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

Кроме того, автоматические тесты могут действовать как первый "настоящий пользователь" вашего кода, заставляя вас строго следить за объявлениями и документированием поведения вашего сайта. Тесты часто являются основой для создания примеров вашего кода и документации. По этим причинам иногда некоторые процессы разработки программного обеспечения начинаются с определения тестов и их реализации, а уже после этого следует написание кода который должен иметь соответствующее поведение (так называемая разработка на основе тестов и на основе поведения).

Данное руководство показывает процесс создания автоматических тестов в Django при помощи добавления их к разработке сайта LocalLibrary.

Типы тестирования

Существует несколько типов, уровней, классификаций тестов и тестовых приёмов. Наиболее важными автоматическими тестами являются:

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

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

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

**Примечание:**К другим типам тестов относятся методы чёрного ящика, белого ящика, ручные, автоматические, канареечные (canary), дымные (smoke), соответствия (conformance), принятия (acceptance), функциональные (functional), системные (system), эффективности (performance), загрузочные (load) и стресс-тесты (stress tests).

Что Django предоставляет для тестирования?

Тестирование сайта это сложная задача, потому что она состоит их нескольких логических слоёв – от HTTP-запроса и запроса к моделям, до валидации формы и их обработки, а кроме того, рендеринга шаблонов страниц.

Django предоставляет фреймворк для создания тестов, построенного на основе иерархии классов, которые, в свою очередь, зависят от стандартной библиотеки Python unittest . Несмотря на название, данный фреймворк подходит и для юнит-, и для интеграционного тестирования. Фреймворк Django добавляет методы API и инструменты, которые помогают тестировать как веб так и, специфическое для Django, поведение. Это позволяет вам имитировать URL-запросы, добавление тестовых данных, а также проводить проверку выходных данных ваших приложений. Кроме того, Django предоставляет API (LiveServerTestCase) и инструменты для применения различных фреймворков тестирования, например вы можете подключить популярный фреймворк Selenium (en-US) для имитации поведения пользователя в реальном браузере.

Для написания теста вы должны наследоваться от любого из классов тестирования Django (или юниттеста) (SimpleTestCase, TransactionTestCase, TestCase, LiveServerTestCase), а затем реализовать отдельные методы проверки кода (тесты это функции-"утверждения", которые проверяют, что результатом выражения являются значения True или False , или что два значения равны и так далее). Когда вы запускаете тест, фреймворк выполняет соответствующие тестовые методы в вашем классе-наследнике. Методы тестирования запускаются независимо друг от друга, начиная с метода настроек и/или завершаясь методом разрушения (tear-down), определённом в классе, как показано ниже.

class YourTestClass(TestCase): def setUp(self): # Установки запускаются перед каждым тестом pass def tearDown(self): # Очистка после каждого метода pass def test_something_that_will_pass(self): self.assertFalse(False) def test_something_that_will_fail(self): self.assertTrue(False) 

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

Примечание: Класс django.test.TestCase очень удобен, но он может приводить к замедленной работе в некоторых случаях (не для каждого теста необходимо настраивать базу данных, или имитировать взаимодействие с отображением). Когда вы познакомитесь с работой данного класса, то сможете заменить некоторые из ваших тестов на более простые классы тестирования.

Что вы должны тестировать?

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

Например, рассмотрим модель Author , определённую ниже. Вам не нужно проверять тот факт, что first_name и last_name были сохранены в базу данных как CharField , потому что за это отвечает непосредственно Django (хотя конечно, на практике в течение разработки вы косвенно будете проверять данную функциональность). Тоже касается и, например, проверки того, что поле date_of_birth является датой, поскольку это тоже часть реализации Django.

Вы должны проверить текст для меток (First name, Last_name, Date of birth, Died), и размер поля, выделенного для текста (100 символов), потому что они являются частью вашей разработки и чем-то, что может сломаться/измениться в будущем.

class Author(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) date_of_birth = models.DateField(null=True, blank=True) date_of_death = models.DateField('Died', null=True, blank=True) def get_absolute_url(self): return reverse('author-detail', args=[str(self.id)]) def __str__(self): return '%s, %s' % (self.last_name, self.first_name) 

Подобным же образом вы должны убедиться, что методы get_absolute_url() и __str__() ведут себя как требуется, потому что они являются частью вашей бизнес логики. В случае функции get_absolute_url() вы можете быть уверены, что функция из Django reverse() была реализована правильно и, следовательно, вы тестируете только то, чтобы соответствующий вызов в отображении был правильно определён.

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

Ну что же, усвоив данную информацию, давайте перейдём к процессу определения и запуска тестов.

Обзор структуры тестов

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

Django использует юнит-тестовый модуль - встроенный "обнаружитель" тестов, который находит тесты в текущей рабочей директории, в любом файле с шаблонным именем test*.py. Предоставляя соответствующие имена файлов, вы можете работать с любой структурой которая вас устраивает. Мы рекомендуем создать пакет для вашего тестирующего кода и, следовательно, отделить файлы моделей, отображений, форм и любые другие, от кода который будет использоваться для тестов. Например:

catalog/ /tests/ __init__.py test_models.py test_forms.py test_views.py

В проекте LocalLibrary создайте файловую структуру, указанную выше. Файл __init__.py должен быть пустым (так мы говорим Питону, что данная директория является пакетом). Вы можете создать три тестовых файла при помощи копирования и переименования файла-образца /catalog/tests.py.

Примечание: Скелет тестового файла /catalog/tests.py был создан автоматически когда мы выполняли построение скелета сайта Django. Является абсолютно "легальным" действием - поместить все ваши тесты в данный файл, тем не менее, если вы проводите тесты "правильно", то вы очень быстро придёте к очень большому и неуправляемому файлу тестирования.

Можете удалить данный файл, поскольку больше он нам не понадобится.

Откройте /catalog/tests/test_models.py. Файл должен импортировать django.test.TestCase , как показано ниже:

from django.test import TestCase # Поместите ваш код тестов здесь 

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

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

class YourTestClass(TestCase): @classmethod def setUpTestData(cls): print("setUpTestData: Run once to set up non-modified data for all class methods.") pass def setUp(self): print("setUp: Run once for every test method to setup clean data.") pass def test_false_is_false(self): print("Method: test_false_is_false.") self.assertFalse(False) def test_false_is_true(self): print("Method: test_false_is_true.") self.assertTrue(False) def test_one_plus_one_equals_two(self): print("Method: test_one_plus_one_equals_two.") self.assertEqual(1 + 1, 2) 

Этот класс определяет два метода которые вы можете использовать для дотестовой настройки (например, создание какой-либо модели, или других объектов, которые вам понадобятся):

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

Примечание: . Классы тестирования также содержат метод tearDown() , который мы пока не используем. Этот метод не особенно полезен для тестирования баз данных, поскольку базовый класс TestCase автоматически разрывает соединения с ними.

Далее идут несколько методов, которые используют функции Assert , проверяющие условия "истинно" (true), "ложно" (false) или равенство ( AssertTrue , AssertFalse , AssertEqual ). Если условия не выполняются как ожидалось, то это приводит к провалу теста и выводу соответствующего сообщения об ошибке на консоль.

Функции проверки утверждений AssertTrue , AssertFalse , AssertEqual реализованы в unittest. В данном фреймворке существуют и другие подобные функции, а кроме того, специфические для Django функции проверки, например, перехода из/к отображению ( assertRedirects ), проверки использования какого-то конкретного шаблона ( assertTemplateUsed ) и так далее.

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

Как запускать тесты

Простейшим способом запуска всех тестов является применение следующей команды:

test 

Таким образом мы найдём в текущей директории все файлы с именем test*.py и запустим все тесты (у нас имеются несколько файлов для тестирования, но на данный момент, только /catalog/tests/test_models.py содержит какие-либо тесты). По умолчанию, тесты сообщат что-нибудь, только в случае провала.

Запустите тесты из корневой папки сайта LocalLibrary. Вы должны увидеть вывод, который похож на следующий.

>python manage.py test Creating test database for alias 'default'... setUpTestData: Run once to set up non-modified data for all class methods. setUp: Run once for every test method to setup clean data. Method: test_false_is_false. .setUp: Run once for every test method to setup clean data. Method: test_false_is_true. .setUp: Run once for every test method to setup clean data. Method: test_one_plus_one_equals_two. . ====================================================================== FAIL: test_false_is_true (catalog.tests.tests_models.YourTestClass) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\Github\django_tmp\library_w_t_2\locallibrary\catalog\tests\tests_models.py", line 22, in test_false_is_true self.assertTrue(False) AssertionError: False is not true ---------------------------------------------------------------------- Ran 3 tests in 0.075s FAILED (failures=1) Destroying test database for alias 'default'... 

Как видите, один тест провалился и мы можем точно увидеть в какой именно функции это произошло и почему (так и было задумано, поскольку False не равен True !).

Примечание: Самая важная вещь, которую нужно извлечь из тестового выхода выше, заключается в том, что это гораздо более ценно, если вы используете описательные/информативные имена для ваших объектов и методов.

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

Следующий раздел показывает как запускать отдельные тесты и как контролировать процесс вывода информации.

Ещё больше тестовой информации

Если вы желаете получать больше информации о тестах вы должны изменить значение параметра verbosity. Например, для вывода списка успешных и неуспешных тестов (и всю информацию о том, как прошла настройка базы данных) вы можете установить значение verbosity равным "2":

test --verbosity 2 

Доступными значениями для verbosity являются 0, 1 (значение по умолчанию), 2 и 3.

Запуск определённых тестов

Если вы хотите запустить подмножество тестов, тогда вам надо указать полный путь к вашему пакету, модулю/подмодулю, классу наследнику TestCase , или методу:

test catalog.tests # Run the specified module python3 manage.py test catalog.tests.test_models # Run the specified module python3 manage.py test catalog.tests.test_models.YourTestClass # Run the specified class python3 manage.py test catalog.tests.test_models.YourTestClass.test_one_plus_one_equals_two # Run the specified method 

Тестирование LocalLibrary

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

**Примечание:**Мы не будем расписывать все тесты, а просто покажем вам пример того, как они должны работать и что ещё вы можете с ними сделать.

Модели

Как было отмечено ранее, мы должны тестировать все то, что является частью нашего кода, а не библиотеки/код, которые уже были протестированы командами разработчиков Django, или Python.

Рассмотрим модель Author . Мы должны провести тесты текстовых меток всех полей, поскольку, даже несмотря на то, что не все они определены, у нас есть проект, в котором сказано, что все их значения должны быть заданы. Если мы не проведём их тестирование, тогда мы не будем знать, что данные метки действительно содержат необходимые значения. Мы уверены в том, что Django создаст поле заданной длины, таким образом наши тесты будут проверять нужный нам размер поля, а заодно и его содержимое.

class Author(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) date_of_birth = models.DateField(null=True, blank=True) date_of_death = models.DateField('Died', null=True, blank=True) def get_absolute_url(self): return reverse('author-detail', args=[str(self.id)]) def __str__(self): return '%s, %s' % (self.last_name, self.first_name) 

Откройте файл /catalog/tests/test_models.py и замените все его содержимое кодом, приведённом во фрагменте для тестирования модели Author (фрагмент представлен ниже).

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

from django.test import TestCase # Create your tests here. from catalog.models import Author class AuthorModelTest(TestCase): @classmethod def setUpTestData(cls): #Set up non-modified objects used by all test methods Author.objects.create(first_name='Big', last_name='Bob') def test_first_name_label(self): author=Author.objects.get(id=1) field_label = author._meta.get_field('first_name').verbose_name self.assertEquals(field_label,'first name') def test_date_of_death_label(self): author=Author.objects.get(id=1) field_label = author._meta.get_field('date_of_death').verbose_name self.assertEquals(field_label,'died') def test_first_name_max_length(self): author=Author.objects.get(id=1) max_length = author._meta.get_field('first_name').max_length self.assertEquals(max_length,100) def test_object_name_is_last_name_comma_first_name(self): author=Author.objects.get(id=1) expected_object_name = '%s, %s' % (author.last_name, author.first_name) self.assertEquals(expected_object_name,str(author)) def test_get_absolute_url(self): author=Author.objects.get(id=1) #This will also fail if the urlconf is not defined. self.assertEquals(author.get_absolute_url(),'/catalog/author/1') 

Тесты полей проверяют значения текстовых меток ( verbose_name ), включая их ожидаемую длину. Все методы имеют описательные имена, а их логика придерживается одной и той же структуры:

# Получение объекта для тестирования author=Author.objects.get(id=1) # Получение метаданных поля для получения необходимых значений field_label = author._meta.get_field('first_name').verbose_name # Сравнить значение с ожидаемым результатом self.assertEquals(field_label,'first name') 

Интересно отметить следующее:

  • Мы не можем получить поле verbose_name напрямую через author.first_name.verbose_name , потому что author.first_name является строкой. Вместо этого, нам надо использовать атрибут _meta объекта автора для получения того экземпляра поля, который будет использоваться для получения дополнительной информации.
  • Мы выбрали метод assertEquals(field_label,'first name') вместо assertTrue(field_label == 'first name') , потому что, в случае провала теста, в выводе будет указано какое именно значение содержит метка и это немного облегчит нам задачу по отладке кода.

Примечание: Тесты для текстовых меток last_name и date_of_birth , а также тест длины поля last_name были опущены. Добавьте свою версию этих тестов, соблюдая соглашение об именовании и следуя структуре логики, представленной выше.

Кроме того, нам надо провести тесты наших собственных методов. Они просто проверяют, что имена объектов имеют следующие значения "Last Name, First Name" и что URL-адрес, по которому мы получаем экземпляр Author , такой как ожидается.

def test_object_name_is_last_name_comma_first_name(self): author=Author.objects.get(id=1) expected_object_name = '%s, %s' % (author.last_name, author.first_name) self.assertEquals(expected_object_name,str(author)) def test_get_absolute_url(self): author=Author.objects.get(id=1) #This will also fail if the urlconf is not defined. self.assertEquals(author.get_absolute_url(),'/catalog/author/1') 

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

====================================================================== FAIL: test_date_of_death_label (catalog.tests.test_models.AuthorModelTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\. \locallibrary\catalog\tests\test_models.py", line 32, in test_date_of_death_label self.assertEquals(field_label,'died') AssertionError: 'Died' != 'died' - Died ? ^ + died ? ^ 

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

**Примечание:**Измените значение метки для поля date_of_death (/catalog/models.py) на "died" и перезапустите тесты.

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

Формы

Смысл проведения тестов для форм тот же, что и для моделей; надо проверить весь собственный код и другие особенности проекта, но не то, что касается фреймворка, или сторонних библиотек.

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

Рассмотрим форму для обновления книг. Она имеет только одно поле обновления даты, которое будет иметь текстовую метку и вспомогательный текст, который вам надо проверить.

class RenewBookForm(forms.Form): """ Форма обновления книг для библиотекарей """ renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).") def clean_renewal_date(self): data = self.cleaned_data['renewal_date'] #Проверка, что дата не в прошлом. if data  datetime.date.today(): raise ValidationError(_('Invalid date - renewal in past')) #Если дата в "далёком" будущем (+4 недели) if data > datetime.date.today() + datetime.timedelta(weeks=4): raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead')) # Всегда надо возвращать очищенные данные. return data 

Откройте файл /catalog/tests/test_forms.py и замените весь существующий в нем код, следующим кодом теста для формы RenewBookForm . Мы начали его с импорта нашей формы и некоторых библиотек Python и Django, которые помогут нам провести тесты. Затем, тем же способом как мы делали для моделей, объявляем тестовый класс нашей формы, то есть применяя описательное имя класс наследника TestCase .

from django.test import TestCase # Создайте ваши тесты здесь import datetime from django.utils import timezone from catalog.forms import RenewBookForm class RenewBookFormTest(TestCase): def test_renew_form_date_field_label(self): form = RenewBookForm() self.assertTrue(form.fields['renewal_date'].label == None or form.fields['renewal_date'].label == 'renewal date') def test_renew_form_date_field_help_text(self): form = RenewBookForm() self.assertEqual(form.fields['renewal_date'].help_text,'Enter a date between now and 4 weeks (default 3).') def test_renew_form_date_in_past(self): date = datetime.date.today() - datetime.timedelta(days=1) form_data = 'renewal_date': date> form = RenewBookForm(data=form_data) self.assertFalse(form.is_valid()) def test_renew_form_date_too_far_in_future(self): date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1) form_data = 'renewal_date': date> form = RenewBookForm(data=form_data) self.assertFalse(form.is_valid()) def test_renew_form_date_today(self): date = datetime.date.today() form_data = 'renewal_date': date> form = RenewBookForm(data=form_data) self.assertTrue(form.is_valid()) def test_renew_form_date_max(self): date = timezone.now() + datetime.timedelta(weeks=4) form_data = 'renewal_date': date> form = RenewBookForm(data=form_data) self.assertTrue(form.is_valid()) 

Первые две функции проверяют текст который должны содержать поля label и help_text . Доступ к полю мы получаем при помощи словаря (то есть, form.fields['renewal_date'] ). Отметим, что мы должны проверять содержит ли метка значение None , иначе в поле текста метки вы увидите " None ".

Оставшиеся функции проверяют валидность дат, то есть их нахождение внутри определённого интервала, а также невалидность для значений, которые находятся вне заданного интервала. Для получения исходного значения мы использовали функцию получения текущей даты ( datetime.date.today() ), а также функцию datetime.timedelta() (которая принимает определённое число дней, или недель). Затем мы просто создали форму, передавая ей наши данные и проверяя её на валидность.

Примечание: В данном примере мы не использовали ни базу данных, ни тестовый клиент. Рассмотрите модификацию этих тестов при помощи класса SimpleTestCase.

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

На этом с формами можно закончить; у нас имеются и другие тесты, но они были созданы обобщёнными классами отображения для редактирования! Запустите тесты и убедитесь, что наш код все ещё им соответствует!

Отображения

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

Давайте начнём с одного из простейших отображений которое возвращает список всех авторов. Вы можете его увидеть по URL-адресу /catalog/authors/ (данный URL-адрес можно найти в разделе приложения catalog, в файле настроек urls.py по имени 'authors').

class AuthorListView(generic.ListView): model = Author paginate_by = 10 

Поскольку это обобщённое отображение списка, то почти все за нас делает Django. Если вы доверяете Django, то единственной вещью, которую вам нужно протестировать, является переход к данному отображению по указанному URL-адресу. Таким образом, если вы применяете методику TDD (test-driven development, разработка через тесты), то начните проект с написания тестов, которые будут проверять, что данное отображение выводит всех авторов и, к тому же, например, блоками по 10.

Откройте файл /catalog/tests/test_views.py замените все его содержимое на следующий код теста для класса AuthorListView . Как и ранее, мы импортируем нашу модель и некоторые полезные классы. В методе setUpTestData() мы задаём число объектов класса Author которые мы тестируем при постраничном выводе.

from django.test import TestCase # Create your tests here. from catalog.models import Author from django.urls import reverse class AuthorListViewTest(TestCase): @classmethod def setUpTestData(cls): #Create 13 authors for pagination tests number_of_authors = 13 for author_num in range(number_of_authors): Author.objects.create(first_name='Christian %s' % author_num, last_name = 'Surname %s' % author_num,) def test_view_url_exists_at_desired_location(self): resp = self.client.get('/catalog/authors/') self.assertEqual(resp.status_code, 200) def test_view_url_accessible_by_name(self): resp = self.client.get(reverse('authors')) self.assertEqual(resp.status_code, 200) def test_view_uses_correct_template(self): resp = self.client.get(reverse('authors')) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed(resp, 'catalog/author_list.html') def test_pagination_is_ten(self): resp = self.client.get(reverse('authors')) self.assertEqual(resp.status_code, 200) self.assertTrue('is_paginated' in resp.context) self.assertTrue(resp.context['is_paginated'] == True) self.assertTrue( len(resp.context['author_list']) == 10) def test_lists_all_authors(self): #Get second page and confirm it has (exactly) remaining 3 items resp = self.client.get(reverse('authors')+'?page=2') self.assertEqual(resp.status_code, 200) self.assertTrue('is_paginated' in resp.context) self.assertTrue(resp.context['is_paginated'] == True) self.assertTrue( len(resp.context['author_list']) == 3) 

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

= self.client.get('/catalog/authors/') resp = self.client.get(reverse('authors')) 

Когда мы получаем ответ, то мы извлекаем код статуса, используемый шаблон, "включён" ли постраничный вывод, количество элементов в подмножестве (на странице) и общее число элементов.

Наиболее интересной переменной является resp.context , которая является объектом контекста, который передаётся шаблону из отображения. Он (объект контекста) очень полезен для тестов, поскольку позволяет нам убедиться, что наш шаблон получает все данные которые ему необходимы. Другими словами мы можем проверить, что мы используем правильный шаблон с данными, которые проделывают долгий путь проверок чтобы соответствовать данному шаблону.

Отображения и регистрация пользователей

В некоторых случаях вам нужно провести тесты отображений к которым имеют доступ только зарегистрированные пользователи. Например, LoanedBooksByUserListView очень похоже на наше предыдущее отображение, но доступно только для залогинившихся пользователей и показывает только те записи ( BookInstance) , которые соответствуют текущему пользователю, имеют статус 'on loan' (книга взята домой), а также забронированы.

from django.contrib.auth.mixins import LoginRequiredMixin class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView): """ Обобщённый класс отображения списка взятых книг текущим пользователем """ model = BookInstance template_name ='catalog/bookinstance_list_borrowed_user.html' paginate_by = 10 def get_queryset(self): return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back') 

Добавьте тестовый код следующего фрагмента в /catalog/tests/test_views.py. В нем, для создания нескольких аккаунтов и объектов BookInstance которые будут использоваться в дальнейших тестах, мы используем метод SetUp() (вместе с соответствующими книгами и другими записями). Половина книг бронируется тестовыми пользователями, но в начале для них всех мы устанавливаем статус "доступно". Использование метода SetUp() предпочтительнее чем setUpTestData() , поскольку в дальнейшем мы будем модифицировать некоторые объекты.

Примечание: Метод setUp() создаёт книгу с заданным языком Language , но ваш код может не включать в себя модель Language , поскольку это было домашним заданием. В таком случае просто закомментируйте соответствующие строки. Поступите также и в следующем разделе, посвящённом RenewBookInstancesViewTest.

import datetime from django.utils import timezone from catalog.models import BookInstance, Book, Genre, Language from django.contrib.auth.models import User # Необходимо для представления User как borrower class LoanedBookInstancesByUserListViewTest(TestCase): def setUp(self): # Создание двух пользователей test_user1 = User.objects.create_user(username='testuser1', password='12345') test_user1.save() test_user2 = User.objects.create_user(username='testuser2', password='12345') test_user2.save() # Создание книги test_author = Author.objects.create(first_name='John', last_name='Smith') test_genre = Genre.objects.create(name='Fantasy') test_language = Language.objects.create(name='English') test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language) # Create genre as a post-step genre_objects_for_book = Genre.objects.all() test_book.genre.set(genre_objects_for_book) # Присвоение типов many-to-many напрямую недопустимо test_book.save() # Создание 30 объектов BookInstance number_of_book_copies = 30 for book_copy in range(number_of_book_copies): return_date= timezone.now() + datetime.timedelta(days=book_copy%5) if book_copy % 2: the_borrower=test_user1 else: the_borrower=test_user2 status='m' BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=the_borrower, status=status) def test_redirect_if_not_logged_in(self): resp = self.client.get(reverse('my-borrowed')) self.assertRedirects(resp, '/accounts/login/?next=/catalog/mybooks/') def test_logged_in_uses_correct_template(self): login = self.client.login(username='testuser1', password='12345') resp = self.client.get(reverse('my-borrowed')) # Проверка что пользователь залогинился self.assertEqual(str(resp.context['user']), 'testuser1') # Проверка ответа на запрос self.assertEqual(resp.status_code, 200) # Проверка того, что мы используем правильный шаблон self.assertTemplateUsed(resp, 'catalog/bookinstance_list_borrowed_user.html') 

Если пользователь не залогирован то, чтобы убедиться в том что отображение перейдёт на страницу входа (логирования), мы используем метод assertRedirects , что продемонстрировано в методе test_redirect_if_not_logged_in() . Затем мы осуществляем вход для пользователя и проверяем что полученный статус status_code равен 200 (успешно).

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

def test_only_borrowed_books_in_list(self): login = self.client.login(username='testuser1', password='12345') resp = self.client.get(reverse('my-borrowed')) #Проверка, что пользователь залогинился self.assertEqual(str(resp.context['user']), 'testuser1') #Check that we got a response "success" self.assertEqual(resp.status_code, 200) #Проверка, что изначально у нас нет книг в списке self.assertTrue('bookinstance_list' in resp.context) self.assertEqual( len(resp.context['bookinstance_list']),0) #Теперь все книги "взяты на прокат" get_ten_books = BookInstance.objects.all()[:10] for copy in get_ten_books: copy.status='o' copy.save() #Проверка, что все забронированные книги в списке resp = self.client.get(reverse('my-borrowed')) #Проверка, что пользователь залогинился self.assertEqual(str(resp.context['user']), 'testuser1') #Проверка успешности ответа self.assertEqual(resp.status_code, 200) self.assertTrue('bookinstance_list' in resp.context) #Подтверждение, что все книги принадлежат testuser1 и взяты "на прокат" for bookitem in resp.context['bookinstance_list']: self.assertEqual(resp.context['user'], bookitem.borrower) self.assertEqual('o', bookitem.status) def test_pages_ordered_by_due_date(self): #Изменение статуса на "в прокате" for copy in BookInstance.objects.all(): copy.status='o' copy.save() login = self.client.login(username='testuser1', password='12345') resp = self.client.get(reverse('my-borrowed')) #Пользователь залогинился self.assertEqual(str(resp.context['user']), 'testuser1') #Check that we got a response "success" self.assertEqual(resp.status_code, 200) #Подтверждение, что из всего списка показывается только 10 экземпляров self.assertEqual( len(resp.context['bookinstance_list']),10) last_date=0 for copy in resp.context['bookinstance_list']: if last_date==0: last_date=copy.due_back else: self.assertTrue(last_date  copy.due_back) 

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

Тестирование форм и отображений

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

В качестве демонстрации давайте напишем некоторые тесты для отображения, которые отвечают за обновление книг( renew_book_librarian() ):

from .forms import RenewBookForm @permission_required('catalog.can_mark_returned') def renew_book_librarian(request, pk): """ Функция отображения обновления экземпляра BookInstance библиотекарем """ book_inst=get_object_or_404(BookInstance, pk = pk) # Если это POST-запрос, тогда обработать данные формы if request.method == 'POST': # Создать объект формы и заполнить её данными из запроса (связывание/биндинг): form = RenewBookForm(request.POST) # Проверка валидности формы: if form.is_valid(): # process the data in form.cleaned_data as required (here we just write it to the model due_back field) book_inst.due_back = form.cleaned_data['renewal_date'] book_inst.save() # переход по URL-адресу: return HttpResponseRedirect(reverse('all-borrowed') ) # Если это GET-запрос (или что-то ещё), то создаём форму по умолчанию else: proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3) form = RenewBookForm(initial='renewal_date': proposed_renewal_date,>) return render(request, 'catalog/book_renew_librarian.html', 'form': form, 'bookinst':book_inst>) 

Нам надо проверить что к данному отображению имеют доступ только те пользователи, которые имеют разрешение типа can_mark_returned , а кроме того, что пользователи перенаправляются на страницу ошибки HTTP 404 если они пытаются обновить экземпляр книги BookInstance , который не существует. Мы должны проверить что начальное значение формы соответствует дате через 3 недели в будущем, а также то, что если форма прошла валидацию, то мы переходим на страницу отображения книг "all-borrowed" (забронированных). Для тестов, отвечающих за проверку "провалов", мы также должны удостовериться что они отправляют соответствующие сообщения об ошибках.

В нижнюю часть файла /catalog/tests/test_views.py добавьте класс тестирования (показан во фрагменте, ниже). Он создаёт двух пользователей и два экземпляра книги, но только один пользователь получает необходимый доступ к соответствующему отображению. Код, который "присваивает" соответствующий доступ, выделен в коде жирным:

from django.contrib.auth.models import Permission # Required to grant the permission needed to set a book as returned. class RenewBookInstancesViewTest(TestCase): def setUp(self): #Создание пользователя test_user1 = User.objects.create_user(username='testuser1', password='12345') test_user1.save() test_user2 = User.objects.create_user(username='testuser2', password='12345') test_user2.save() permission = Permission.objects.get(name='Set book as returned') test_user2.user_permissions.add(permission) test_user2.save() #Создание книги test_author = Author.objects.create(first_name='John', last_name='Smith') test_genre = Genre.objects.create(name='Fantasy') test_language = Language.objects.create(name='English') test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,) #Создание жанра Create genre as a post-step genre_objects_for_book = Genre.objects.all() test_book.genre=genre_objects_for_book test_book.save() #Создание объекта BookInstance для для пользователя test_user1 return_date= datetime.date.today() + datetime.timedelta(days=5) self.test_bookinstance1=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user1, status='o') #Создание объекта BookInstance для для пользователя test_user2 return_date= datetime.date.today() + datetime.timedelta(days=5) self.test_bookinstance2=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user2, status='o') 

В нижнюю часть класса тестирования добавьте следующие методы (из следующего фрагмента). Они проверяют, что только пользователь с соответствующим доступом (testuser2) имеет доступ к отображению. Мы проверяем все случаи: когда пользователь не залогинился, когда залогинился, но не имеет соответствующего доступа, когда имеет доступ, но не является заёмщиком книги (тест должен быть успешным), а также, что произойдёт если попытаться получить доступ к книге BookInstance которой не существует. Кроме того, мы проверяем то, что используется правильный (необходимый) шаблон.

def test_redirect_if_not_logged_in(self): resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>) ) #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable) self.assertEqual( resp.status_code,302) self.assertTrue( resp.url.startswith('/accounts/login/') ) def test_redirect_if_logged_in_but_not_correct_permission(self): login = self.client.login(username='testuser1', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>) ) #Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable) self.assertEqual( resp.status_code,302) self.assertTrue( resp.url.startswith('/accounts/login/') ) def test_logged_in_with_permission_borrowed_book(self): login = self.client.login(username='testuser2', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance2.pk,>) ) #Check that it lets us login - this is our book and we have the right permissions. self.assertEqual( resp.status_code,200) def test_logged_in_with_permission_another_users_borrowed_book(self): login = self.client.login(username='testuser2', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>) ) #Check that it lets us login. We're a librarian, so we can view any users book self.assertEqual( resp.status_code,200) def test_HTTP404_for_invalid_book_if_logged_in(self): import uuid test_uid = uuid.uuid4() #unlikely UID to match our bookinstance! login = self.client.login(username='testuser2', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':test_uid,>) ) self.assertEqual( resp.status_code,404) def test_uses_correct_template(self): login = self.client.login(username='testuser2', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>) ) self.assertEqual( resp.status_code,200) #Check we used correct template self.assertTemplateUsed(resp, 'catalog/book_renew_librarian.html') 

Добавьте ещё один тестовый метод, показанный ниже. Он проверяет что начальная дата равна трём неделям в будущем. Заметьте, что мы имеем возможность получить доступ к начальному значению из поля формы (выделено жирным).

def test_form_renewal_date_initially_has_date_three_weeks_in_future(self): login = self.client.login(username='testuser2', password='12345') resp = self.client.get(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>) ) self.assertEqual( resp.status_code,200) date_3_weeks_in_future = datetime.date.today() + datetime.timedelta(weeks=3) self.assertEqual(resp.context['form'].initial['renewal_date'], date_3_weeks_in_future ) 

Следующий тест (тоже добавьте его в свой класс) проверяет что отображение, в случае успеха, перенаправляет пользователя к списку всех забронированных книг. Здесь мы показываем как при помощи клиента вы можете создать и передать данные в POST -запросе. Данный запрос передаётся вторым аргументом в пост-функцию и представляет из себя словарь пар ключ/значение.

def test_redirects_to_all_borrowed_book_list_on_success(self): login = self.client.login(username='testuser2', password='12345') valid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=2) resp = self.client.post(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>), 'renewal_date':valid_date_in_future> ) self.assertRedirects(resp, reverse('all-borrowed') ) 

Предупреждение: Вместо перехода к отображению all-borrowed, добавленного в качестве домашнего задания, вы можете перенаправить пользователя на домашнюю страницу '/'. В таком случае, исправьте две последние строки тестового кода на код, показанный ниже. Присваивание follow=True , в запросе, гарантирует что запрос вернёт окончательный URL-адрес пункта назначения (следовательно проверяется /catalog/ , а не / ).

= self.client.post(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>), 'renewal_date':valid_date_in_future>,follow=True ) self.assertRedirects(resp, '/catalog/') 

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

def test_form_invalid_renewal_date_past(self): login = self.client.login(username='testuser2', password='12345') date_in_past = datetime.date.today() - datetime.timedelta(weeks=1) resp = self.client.post(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>), 'renewal_date':date_in_past> ) self.assertEqual( resp.status_code,200) self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal in past') def test_form_invalid_renewal_date_future(self): login = self.client.login(username='testuser2', password='12345') invalid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=5) resp = self.client.post(reverse('renew-book-librarian', kwargs='pk':self.test_bookinstance1.pk,>), 'renewal_date':invalid_date_in_future> ) self.assertEqual( resp.status_code,200) self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal more than 4 weeks ahead') 

Такие же способы тестирования могут применяться для проверок других отображений.

Шаблоны

Django предоставляет API для тестирования, которое проверяет что функции отображения вызывают правильные шаблоны, а также позволяют убедиться, что им передаётся соответствующая информация. Кроме того, в Django имеется возможность использовать сторонние API для проверок того, что ваш HTML показывает то, что надо.

Другие рекомендованные инструменты для тестирования

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

Из всего множества сторонних инструментов тестирования, мы кратко опишем возможности двух:

  • Coverage: Это инструмент Python, который формирует отчёты о том, какое количество кода выполняется во время проведения тестов. Это полезно для уточнения степени "покрытия" кода тестами.
  • Selenium (en-US) это фреймворк проведения автоматического тестирования в настоящем браузере. Он позволяет вам имитировать взаимодействие пользователя с вашим сайтом (что является следующим шагом в проведении интеграционных тестов).

Домашняя работы

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

class AuthorCreate(PermissionRequiredMixin, CreateView): model = Author fields = '__all__' initial='date_of_death':'12/10/2016',> permission_required = 'catalog.can_mark_returned' 

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

Итоги

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

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

Следующая и последняя часть руководства покажет вам как запустить ваш чудесный (и полностью протестированный!) веб-сайт Django.

Смотрите также

  • Написание и запуск тестов (Django docs)
  • Написание вашего первого приложения Django, часть 5 > Введение в автоматическое тестирование (Django docs)
  • Инструменты для тестирования (Django docs)
  • Продвинутое тестирование (Django docs)
  • Путеводитель по тестированию в Django (Toast Driven Blog, 2011)
  • Мастерская: Разработка через тесты с Django (TDD) (San Diego Python, 2014)
  • Тестирование в Django (Часть 1) - Лучшие практики и Примеры (RealPython, 2013)
  • Назад
  • Обзор: Django
  • Далее

Found a content problem with this page?

  • Edit the page on GitHub.
  • Report the content issue.
  • View the source on GitHub.

This page was last modified on 3 авг. 2023 г. by MDN contributors.

Что происходит, когда вы выполняете manage.py test?

Вы запускаете тесты командой manage.py test , но знаете ли вы, что происходит под капотом при этом? Как работает исполнитель тестов (test runner) и как он расставляет точки, E и F на экране?

Когда вы узнаете, как работает Django, то откроете для себя множество вариантов использования, таких как изменение файлов cookie, установка глобальных заголовков и логирование запросов. Аналогично, поняв то, как работают тесты, вы сможете кастомизировать процессы, чтобы, например, загружать тесты в другом порядке, настраивать параметры тестирования без отдельного файла или блокировать исходящие HTTP-запросы.

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

Однако, прежде чем писать код, давайте проведем реконструкцию процесса тестирования.

Выходные данные тестов

Давайте разберемся в результатах выполнения тестов. За основу возьмем проект с пустым тестом:

from django.test import TestCase class ExampleTests(TestCase): def test_one(self): pass

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

$ python manage.py test Creating test database for alias 'default'. System check identified no issues (0 silenced). . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK Destroying test database for alias 'default'. 

Чтобы понять, что происходит, попросим программу рассказать об этом подробнее, добавив флаг -v 3 :

$ python manage.py test -v 3 Creating test database for alias 'default' ('file:memorydb_default?mode=memory&cache=shared'). Operations to perform: Synchronize unmigrated apps: core Apply all migrations: (none) Synchronizing apps without migrations: Creating tables. Running deferred SQL. Running migrations: No migrations to apply. System check identified no issues (0 silenced). test_one (example.core.tests.test_example.ExampleTests) . ok ---------------------------------------------------------------------- Ran 1 test in 0.004s OK Destroying test database for alias 'default' ('file:memorydb_default?mode=memory&cache=shared'). 

Отлично, этого достаточно! Теперь давайте разбираться.

На первой строке мы видим сообщение «Creating test database…» - так Django отчитывается о создании тестовой базы данных. Если в вашем проекте несколько баз данных, вы увидите по одной строке для каждой.

В этом проекте я использую SQLite, поэтому Django автоматически ставит mode=memory в поле адреса базы данных. Так операции с базой данных станут быстрее примерно раз в 10. Другие базы данных, такие как PostgreSQL, не имеют подобных режимов, но для них есть другие методы запуска in-memory.

Вторая строка «Operations to perform» и несколько последующих строк – это выходные данные команды migrate в тестовых базах данных. То есть вывод тут получается идентичным с тем, который мы получаем при выполнении manage.py migrate на пустой базе данных. Сейчас я использую небольшой проект без миграций, но если бы они были, то на каждую миграцию в выводе приходилась бы одна строка.

Дальше идет строка с надписью «System check identified no issues». Он взялась из Django, который запускает ряд «проверок перед полетом», чтобы убедиться в правильной конфигурации вашего проекта. Вы можете запустить проверку отдельно с помощью команды manage.py check , и также она выполнится автоматически с запуском большинства команд управления. Однако в случае с тестами, она будет отложена до тех пор, пока тестовые базы данных не будут готовы, так как некоторые этапы проверки используют соединения баз данных.

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

Следующие строки про наши тесты. По умолчанию test runner выводит по одному символу на тест, но с повышением verbosity в Django для каждого теста будет выводиться отдельная строка. Здесь у нас есть всего один тест «testone», и когда он закончил выполнение, test runner добавил к строке «ok».

Чтобы обозначить конец прогона, ставится разделитель «---». Если бы у нас были какие-то ошибки или сбои, они бы вывелись перед этими черточками. После этого идет краткое описание выполненных тестов и «OK», показывающее, что тесты прошли успешно.

Последняя строка сообщает нам, что тестовая база данных была удалена.

В итоге у нас получается следующая последовательность шагов:

  1. Создание тестовых баз данных.
  2. Миграция баз данных.
  3. Запуск проверок системы.
  4. Запуск тестов.
  5. Отчет о количестве тестов и успешном/неуспешном завершении.
  6. Удаление тестовых баз данных.

Давайте разберемся, какие компоненты внутри Django отвечают за эти шаги.

Django и unittest

Как вам, должно быть, уже известно, фреймворк для тестирования в Django расширяет фреймворк unittest из стандартной библиотеки Python. Каждый компонент, отвечающий за шаги, описанные выше, либо встроен в unittest, либо является одним из расширений Django. Мы можем отразить это на диаграмме:

Мы можем найти компоненты каждой стороны взглянув на код.

Команда управления тестами «test»

Первое, на что нужно посмотреть, — это команда управления тестами, которую Django находит и выполняет при запуске manage.py test . Находится она в django.core.management.commands.test .

Что касается команд управления, то они довольно короткие – меньше 100 строк. Метод handle() отвечает за обработку в TestRunner. Если упрощать до трех основных строк:

def handle(self, *test_labels, **options): TestRunner = get_runner(settings, options['testrunner']) . test_runner = TestRunner(**options) . failures = test_runner.run_tests(test_labels) . 

Так что же представляет из себя класс TestRunner? Это компонент Django, который координирует процесс тестирования. Он настраиваемый, но класс по умолчанию, и единственный в самом Django – это django.test.runner.DiscoverRunner . Давайте рассмотрим его следующим.

Класс DiscoverRunner

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

Начинается он как-то так:

class DiscoverRunner: """A Django test runner that uses unittest2 test discovery.""" test_suite = unittest.TestSuite parallel_test_suite = ParallelTestSuite test_runner = unittest.TextTestRunner test_loader = unittest.defaultTestLoader

Эти атрибуты класса указывают на другие классы, которые выполняют различные шаги в процессе тестирования. Как видите, большинство из них – компоненты unittest.

Обратите внимание на то, что один из них называется test_runner, таким образом у нас получается два различных понятия, который называются «test runner» — это DiscoverRunner из Django и TextTestRunner из unittest. DiscoverRunner делает гораздо больше, чем TextTestRunner , и у него другой интерфейс. Возможно, в Django можно было бы обозвать DiscoverRunner по-другому, например, TestCoordinator, но сейчас об этом уже поздно думать.

Основной поток в DiscoverRunner находится в методе runtests() . Если убрать кучу деталей, run_tests() будет выглядеть примерно так:

def run_tests(self, test_labels, extra_tests=None, **kwargs): self.setup_test_environment() suite = self.build_suite(test_labels, extra_tests) databases = self.get_databases(suite) old_config = self.setup_databases(aliases=databases) self.run_checks(databases) result = self.run_suite(suite) self.teardown_databases(old_config) self.teardown_test_environment() return self.suite_result(suite, result)

Шагов здесь совсем немного. Многие из методов соответствуют шагам из списка, который мы приводили выше:

  • setup_databases() создает тестовые базы данных. Но этот метод создает только те базы данных, которые необходимы для выбранных тестов, отфильтрованных с помощью get_databases() , поэтому если вы запускаете только SimpleTestCases без баз данных, то Django ничего создавать не будет. Внутри этого метода создаются базы данных и выполняется команда migrate .
  • run_checks() запускает проверки.
  • run_suite() запускает набор тестов, включая все выходные данные.
  • Функция teardown_databases() удаляет тестовые базы данных.

И еще парочка методов, о которых можно рассказать:

  • setup_test_environment() и teardown_test_environment() устанавливают или убирают некоторые настройки, такие как локальный сервер электронной почты.
  • suite_result() возвращает количество ошибок в ответ команде управления тестированием.

Все эти методы полезно рассмотреть, чтобы разобраться с настройками процесса тестирования. Но они все являются частью Django. Другие методы передаются компонентам в unittest - build_suite() и run_suite() .

Давайте поговорим и о них.

buildsuite()

buildsuite() ищет тесты для запуска и перемещает их в объект «suite». Это длинный метод, но если его упростить, выглядеть он будет примерно так:

def build_suite(self, test_labels=None, extra_tests=None, **kwargs): suite = self.test_suite() test_labels = test_labels or ['.'] for label in test_labels: tests = self.test_loader.loadTestsFromName(label) suite.addTests(tests) if self.parallel > 1: suite = self.parallel_test_suite(suite, self.parallel, self.failfast) return suite

В этом методе используются три из четырех классов, к которым, как мы видели, обращается DiscoverRunner:

  • test_suite - компонент unittest, который служит контейнером для запуска тестов.
  • parallel_test_suite - оболочка для набора тестов, которая используется с функцией параллельного тестирования в Django.
  • test_loader – компонент unittest , который умеет находить тестовые модули на диске и загружать их в набор.

runsuite()

Еще один метод DiscoverRunner, о котором надо поговорить – это run_suite() . Его мы упрощать не будем, и просто посмотрим, как он выглядит:

def run_suite(self, suite, **kwargs): kwargs = self.get_test_runner_kwargs() runner = self.test_runner(**kwargs) return runner.run(suite)

Его единственная задача – создавать test runner и говорить ему запустить собранный набор тестов. Это последний из компонентов unittest, на который ссылается атрибут класса. Он использует unittest.TextTestRunner - test runner по умолчанию для вывода результатов в виде текста, в отличие, например, от XML-файла для передачи результатов в вашу CI-систему.

Закончим мы наше небольшое расследование, заглянув в класс TextTestRunner .

Класс TextTestRunner

Этот компонент unittest берет тест-кейс или набор и выполняет его. Начинается он вот так:

class TextTestRunner(object): """A test runner class that displays results in textual form. It prints out the names of tests as they are run, errors as they occur, and a summary of the results at the end of the test run. """ resultclass = TextTestResult def __init__(self, . resultclass=None, . ):

По аналогии с DiscoverRunner , он использует атрибут класса для ссылки на другой класс. Класс TextTestResult по умолчанию отвечает за текстовый вывод. В отличие от ссылок класса DiscoverRunner , мы можем переопределить resultclass , передав альтернативу TextTestRunner._init_() .

Теперь мы наконец-то можем кастомизировать процесс тестирования. Но сначала вернемся к нашему маленькому исследованию.

Карта

Теперь мы можем расширить карту и показать на ней классы, которые мы нашли:

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

Как кастомизировать

Django предлагает два способа кастомизации процесса выполнения тестов:

  • Переопределить команду тестирования с помощью кастомного подкласса.
  • Переопределить класс DiscoverRunner, указав в настройках TESTRUNNER кастомный подкласс.

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

Супербыстрый Test Runner

В качестве базового примера давайте представим, что нам нужно пропускать тесты и каждый раз уведомлять об успешном прохождении теста. Сделать это мы можем создав подкласс DiscoverRunner с новым методом runtests() , который не будет вызывать метод super() :

# example/test.py from django.test.runner import DiscoverRunner class SuperFastTestRunner(DiscoverRunner): def run_tests(self, *args, **kwargs): print("All tests passed! A+") failures = 0 return failures

Затем воспользуемся им в файле с настройками следующим образом:

TEST_RUNNER = "example.test.SuperFastTestRunner"

А затем выполним manage.py test , и получим результат за рекордно короткое время!

$ python manage.py test All tests passed! A+

Отлично, очень полезно!

А теперь давайте сделаем еще практичнее, и перейдем к выводу результатов теста в виде эмодзи!

Вывод в виде эмодзи

Мы уже выяснили, что компонент TextTestResult из unittest отвечает за вывод. Мы можем заменить его в DiscoverRunner , передав значение resultclass в TextTestRunner .

В Django уже есть опции для замены resultclass , например, опция --debug-sql option, которая выводит выполненные запросы для неудачных тестов.

DiscoverRunner.run_suite() создает TextTestRunner с аргументами из метода DiscoverRunner.get_test_runner_kwargs() :

def get_test_runner_kwargs(self): return

Он же в свою очередь вызывает get_resultclass() , который возвращает другой класс, если был использован один из двух параметров тестовой команды ( --debug-sql или --pdb ):

def get_resultclass(self): if self.debug_sql: return DebugSQLTextTestResult elif self.pdb: return PDBDebugResult

Если ни один из параметров не задан, метод неявно возвращает None, говоря TextTestResult использовать по умолчанию resultclass . Мы можем увидеть этот None в нашем собственном подклассе и заменить его подклассом TextTestResult :

class EmojiTestRunner(DiscoverRunner): def get_resultclass(self): klass = super().get_resultclass() if klass is None: return EmojiTestResult return klass

Наш класс EmojiTestResult расширяет TextTestResult и заменяет вывод точек по умолчанию на эмодзи. В конечном итоге получается довольно длинно, поскольку на каждый тип результата приходится отдельный метод:

class EmojiTestResult(unittest.TextTestResult): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # If the "dots" style was going to be used, show emoji instead self.emojis = self.dots self.dots = False def addSuccess(self, test): super().addSuccess(test) if self.emojis: self.stream.write('✅') self.stream.flush() def addError(self, test, err): super().addError(test, err) if self.emojis: self.stream.write('?') self.stream.flush() def addFailure(self, test, err): super().addFailure(test, err) if self.emojis: self.stream.write('❌') self.stream.flush() def addSkip(self, test, reason): super().addSkip(test, reason) if self.emojis: self.stream.write("⏭") self.stream.flush() def addExpectedFailure(self, test, err): super().addExpectedFailure(test, err) if self.emojis: self.stream.write("❎") self.stream.flush() def addUnexpectedSuccess(self, test): super().addUnexpectedSuccess(test) if self.emojis: self.stream.write("✳️") self.stream.flush() def printErrors(self): if self.emojis: self.stream.writeln() super().printErrors()

После указания TEST_RUNNER в EmojiTestRunner , мы можем запустить тесты и увидеть эмодзи:

$ python manage.py test Creating test database for alias 'default'. System check identified no issues (0 silenced). ?❎❌⏭✅✅✅✳️ . ---------------------------------------------------------------------- Ran 8 tests in 0.003s FAILED (failures=1, errors=1, skipped=1, expected failures=1, unexpected successes=1) Destroying test database for alias 'default'. 

Вместо заключения

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

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

Я знаком лишь с двумя библиотеками, предоставляющими кастомные подклассы DiscoverRunner :

  • unittest-xml-reporting обеспечивает вывод в формате XML для вашей CI-системы.
  • django-slow-tests обеспечивает измерение времени выполнения тесты для поиска самых медленных тестов.

Сам я не пробовал, но их объединение может не сработать, поскольку обе они переопределяют процесс вывода.

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

Если вам интересна более детальная настройка процесса тестирования, обратитесь к pytest.

Конец

Спасибо, что отправились со мной в это путешествие. Я надеюсь, что вы узнали что-то новое о том, как Django запускает ваши тесты, и научились кастомизировать их.

Читать ещё:

Чем отличается unittest в django от стандартной python библиотеки unittest

МЕРОПРИЯТИЯ

Moscow QA #1

Комментарии

Популярные По порядку
Не удалось загрузить комментарии.

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

ООП на Python: концепции, принципы и примеры реализации

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

3 самых важных сферы применения Python: возможности языка

Существует множество областей применения Python, но в некоторых он особенно хорош. Разбираемся, что же можно делать на этом ЯП.

Программирование на Python: от новичка до профессионала

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Luxury Car Rental in Miami | Reserve a Premium Vehicle with RealCar