Юнит-тесты в Django
Библиотека Unittest так понравилась разработчикам Django, что они встроили её в свой фреймворк как штатный инструмент для тестирования, расширив эту библиотеку специализированными методами для тестирования форм, классов и view-функций.
Где живут тесты Django
При выполнении команды «создать новое приложение» python manage.py startapp <app_name>
в директории приложения автоматически создается файл для тестов tests.py
:
└── <app_name>
├── __init.py__
├── admin.py
├── models.py
├── tests.py # Место для тестов
└── views.py
Это лучше, чем ничего, но на практике размещение всех тестов в одном файле приведёт к тому, что файл раздуется и станет плохо читаемым.
Лучше делать так:
- Удалить файл tests.py
- Создать в директории приложения пакет
tests
: создать директорию /tests и разместить в нём пустой файл__init__.py
- В этом пакете создать отдельные файлы для тестов: файл для тестирования моделей, файл для views, файл для форм, файл для проверки URL.
└── <app_name>
├── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_models.py # Тесты моделей
│ ├── test_urls.py # Тесты адресов
│ ├── test_forms.py # Тесты форм
│ └── test_views.py # Тесты представлений
├── admin.py
├── models.py
└── views.py
Наследование и test runner
Перед началом работы в код файлов с тестами импортируется класс TestCase из пакета django.test. Все классы тестов должны наследоваться от него.
# Каждый логический набор тестов — это класс,
# который наследуется от базового класса TestCase
from django.test import TestCase
class Test(TestCase):
def test_example(self):
# пишем тест тут
...
В Django встроен собственный test runner, при запуске он ищет тесты в пакетах текущей директории, в файлах с именем test*.py (вместо символа «звёздочка» может быть любой набор символов).
Благодаря такому алгоритму test runner Django сможет найти тесты и в <app_name>/test.py, и в <app_name>/tests/test.py, и в <app_name>/tests/test_views.py
Добавьте в приложение posts проекта Yatube директории и пустые файлы в соответствии с приведённой выше структурой.
Перед выполнением тестов убедитесь, что виртуальное окружение проекта запущено. В консоли из корневой папки проекта Yatube выполните команду python3 manage.py test
. Запустится test runner.
В консоли будет выведен примерно такой ответ:
(venv) $ python3 manage.py test
System check identified no issues (0 silenced).
----------------------------------------------------------------------
Ran 0 tests in 0.000s
OK
Логично: нет тестов — нет ошибок.
Первое тестирование: smoke testing
При проверке новых устройств радиолюбители проводят Smoke Test (англ. «дымовое тестирование»): при первом включении аппарата питание подаётся на очень короткое время, меньше секунды. Если устройство задымило — понятно, что дальнейшие проверки не нужны: что-то сломалось, надо чинить прямо сейчас.
«Дымовое тестирование» в Django выглядит так: проверяем, что главная страница доступна (возвращает статус 200).
Получилось? Отлично, пишем следующие тесты. Не получилось — проект не работает, ищем ошибку, а тесты подождут.
Проверку статуса страницы проводят через программный HTTP-клиент, имитирующий работу браузера.
Программный HTTP-клиент
При мануальном тестировании вы через браузер отправляли запросы к тем или иным адресам проекта и смотрели, что отображается в браузере.
При программном тестировании в Django можно эмулировать браузер (веб-клиент) прямо в коде — и тестировать проект через него.
Этот программный клиент может:
- имитировать GET- и POST-запросы, проверять заголовки ответов и содержимое страниц;
- работать от имени авторизованного или неавторизованного пользователя;
- отслеживать редиректы и проверять URL и код статуса при каждом редиректе;
- проверять, какие HTML-шаблоны применяются для рендера запрошенной страницы и что передаётся в словаре context;
Для создания программного клиента в модуле django.test
есть класс Client()
. Каждый экземпляр этого класса — это отдельный веб-клиент, которым можно управлять из кода.
При тестировании можно создать несколько таких клиентов: в одном можно авторизоваться, а из другого клиента работать без авторизации, тестируя сценарии для анонимных пользователей.
Запрос к проекту через Client()
Запустите виртуальное окружение проекта; активируйте Django Python shell (интерактивный режим Django): $ python3 manage.py shell
Вот как будет выглядеть smoke testing проекта Yatube:
(venv) $ python manage.py shell
Python 3.8.0 (default, Nov 22 2020, 23:37:58)
[Clang 11.0.0 (clang-1100.0.33.12)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.test import Client
# Создаём объект класса Client(), эмулятор веб-браузера
>>> guest_client = Client()
# — Браузер, сделай GET-запрос к главной странице
>>> response = guest_client.get('/')
# Какой код вернула страница при запросе?
>>> response.status_code
200 # Страница работает: она вернула код 200
Объект response: ответ на запрос
При запросе клиента возвращается специальный объект response. В нём содержится ответ сервера и дополнительная информация:
status_code
— содержит код ответа запрошенного адреса;client
— объект клиента, который использовался для обращения;content
— данные ответа в виде строки байтов;context
— словарь переменных, переданный для отрисовки шаблона при вызове функцииrender()
;request
— объектrequest
, первый параметр view-функции, обработавшей запрос;templates
— перечень шаблонов, вызванных для отрисовки запрошенной страницы;resolver_match
— специальный объект, соответствующийpath()
из спискаurlpatterns
.
Время настоящих тестов
Теперь тесты можно выполнить не из консоли, а из файла.В проекте Yatube создайте файл posts/tests/test_urls.py
и добавьте в него код тестирующего класса:
# posts/tests/tests_url.py
from django.test import TestCase, Client
class StaticURLTests(TestCase):
def test_homepage(self):
# Создаем экземпляр клиента
guest_client = Client()
# Делаем запрос к главной странице и проверяем статус
response = guest_client.get('/')
# Утверждаем, что для прохождения теста код должен быть равен 200
self.assertEqual(response.status_code, 200)
Запустите тест командой python3 manage.py test
При тестировании не нужно запускать сервер разработчика: test runner Django сам всё сделает.
В консоли должен появиться примерно такой ответ:
Отлично, код «не дымит», smoke test пройден. Можно писать следующие тесты.
Выборочный запуск тестов
Иногда нет необходимости выполнять все тесты проекта, а нужно запустить лишь один тест или определённую группу тестов. Для такого выборочного запуска можно через точечную нотацию указать путь к нужному пакету, модулю, тестирующему классу или методу:
# Запустит все тесты проекта
python3 manage.py test
# Запустит только тесты в приложении posts
python3 manage.py test posts
# Запустит только тесты из файла test_urls.py в приложении posts
python3 manage.py test posts.tests.test_urls
# Запустит только тесты из класса StaticURLTests для test_urls.py в приложении posts
python3 manage.py test posts.tests.test_urls.StaticURLTests
# Запустит только тест test_homepage()
# из класса StaticURLTests для test_urls.py в приложении posts
python3 manage.py test posts.tests.test_urls.StaticURLTests.test_homepage
Выборочный запуск тестов полезен, когда нужно протестировать какой-то конкретный фрагмент кода и не хочется тратить время на выполнение остальных тестов.
Больше информации о результатах теста
Команду python3 manage.py test
можно запустить с параметром --verbosity
(есть сокращённая запись этого параметра: -v
), значениями которого могут быть числа от 0 до 3. Этот параметр отвечает за детализацию отчёта о тестах.
Если этот параметр не указан явно — по умолчанию он устанавливается равным единице:
python3 manage.py test
# Это то же самое, что
python3 manage.py test -v 1
Чтобы увидеть развёрнутый список пройденных и проваленных тестов — установите --verbosity 2
:
python3 manage.py test -v 2
Погоняйте тесты с различными значениями verbosity
и посмотрите, чем отличается вывод результатов; выберите удобный формат.
Подготовка данных для тестирования
Как и в Unittest для Python, в django.test можно предустановить фикстуры, исходные данные для тестирования. Для этого применяются те же методы, что и в Unittest: setUp()
и setUpClass()
.
В проведённом тесте клиент был создан прямо в методе test_homepage()
. Если это единственный тест в классе — проблем нет. Но когда в классе будет пять-десять тестов — гораздо удобнее один раз создать клиент (а лучше — два: авторизованный и неавторизованный), и затем работать с ними в тестах.
# posts/tests/tests_url.py
from django.test import TestCase, Client
class StaticURLTests(TestCase):
def setUp(self):
# Устанавливаем данные для тестирования
# Создаём экземпляр клиента. Он неавторизован.
self.guest_client = Client()
def test_homepage(self):
# Отправляем запрос через client,
# созданный в setUp()
response = self.guest_client.get('/')
self.assertEqual(response.status_code, 200)
Тестомерки Coverage
Проект не дымит. Что тестировать дальше?
Ответ на этот вопрос даст инструмент coverage (англ. «покрытие»). Он показывает, в какой степени код проекта проверен (покрыт) тестами.
Любимая игра программистов — увеличить coverage до 100%.
Установка пакета Coverage
Пакет coverage устанавливается обычным образом, через pip, но если что-то пойдёт не так — посмотрите инструкцию по установке.
Запустите виртуальное окружение проекта Yatube и в консоли выполните команду pip3 install coverage
.
Installing collected packages: coverage
Successfully installed coverage-5.3
Перейдите в рабочую директорию проекта (где хранится manage.py) и запустите coverage: выполните $ coverage run --source='posts,users' manage.py test -v 2
- Параметр
-source='posts,users'
(без пробела после запятой) ограничит проверку coverage модулями posts и users. - Параметр
--source='.'
запустит проверку coverage всех модулей в текущей директории (символ «точка» означает текущую директорию) и в её субдиректориях. - Если параметр
--source
не указывать — будет проверено покрытие тестами всех модулей проекта, включая /venv. В результате в отчёт будет выведена масса ненужной информации. Лучше явно указывать в параметре--source
те модули или директории, которые нужно проверить. - Для получения большей детализации установите для параметра
verbosity
значение 2.
После выполнения команды $ coverage run --source='posts,users' manage.py test -v 2
coverage сформирует отчёт и сохранит его в корневой папке проекта, в файле .coverage.
└── yatube
├── posts/
├── templates/
├── users/
├── yatube/
├── .coverage <-- попался, отчет!
├── db.sqlite3
└── manage.py
Сам по себе файл .coverage непригоден для чтения и анализа человеком, но из него можно получить отчёты в разных форматах.
Самый простой способ — вывести результаты в консоль. Команда coverage report
покажет отчёт примерно в таком виде:Не радуйтесь стопроцентному покрытию файлов тестами: 100% будет показано, например, для файлов, в которых просто нечего тестировать.
Для представления результатов есть и более удобный формат: отчёт можно сохранить в виде HTML.Команда coverage html
сформирует папку /htmlcov:
└── yatube
├── htmlcov <-- отчёт в HTML-формате
├── posts
├── templates
├── users
├── yatube
├── .coverage
├── db.sqlite3
└── manage.py
Откройте файл yatube/htmlcov/index.html
через браузер, погуляйте по ссылкам, там много интересного.Все команды отображения работают с созданным отчётом .coverage. После нового запуска $ coverage run
этот отчёт будет перезаписан.
Git Ignore
Поскольку coverage — это служебный инструмент, не следует отслеживать файлы этого пакета через Git.Добавьте файлы coverage в .gitignore. Стандартный файл .gitignore для Python, размещённый на GitHub, включает список файлов coverage. Но можно добавить руками этот список в ваш .gitignore:
htmlcov/
.coverage
.coverage.*
coverage.xml
*.cover
Готово, можно меряться своим coverage с коллегами.