Unittest в Django и Coverage-тестомерка

Юнит-тесты в 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       

Это лучше, чем ничего, но на практике размещение всех тестов в одном файле приведёт к тому, что файл раздуется и станет плохо читаемым.

Лучше делать так:

  1. Удалить файл tests.py
  2. Создать в директории приложения пакет tests: создать директорию /tests и разместить в нём пустой файл __init__.py
  3. В этом пакете создать отдельные файлы для тестов: файл для тестирования моделей, файл для 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 можно предустановить фикстуры, исходные данные для тестирования. Для этого применяются те же методы, что и в UnittestsetUp() и 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 2coverage сформирует отчёт и сохранит его в корневой папке проекта, в файле .coverage.

└── yatube
    ├── posts/
    ├── templates/ 
    ├── users/ 
    ├── yatube/
    ├── .coverage <-- попался, отчет!
    ├── db.sqlite3  
    └── manage.py 

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

Самый простой способ — вывести результаты в консоль. Команда coverage report покажет отчёт примерно в таком виде:imageНе радуйтесь стопроцентному покрытию файлов тестами: 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 с коллегами.





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

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: