Запуск первых тестов
В мире Python есть несколько популярных фреймворков тестирования. В стандартную библиотеку языка входит фреймворк unittest, он принят в Django как основа для написания тестов. Помимо него есть и другие библиотеки, но мы будем тестировать код стандартными инструментами.
При создании нового приложения в его директории появился файл tests.py. Он пуст, но уже включен в инфраструктуру тестирования проекта.
Давайте запустим все тесты проекта (спойлер: их нет).
Скопировать кодBASH
(venv) $ python manage.py test
System check identified no issues (0 silenced).
Ran 0 tests in 0.000s
OK
Тестирование в Django организовано достаточно удобно и гибко. При выполнении команды общего запуска тестов система ищет файл tests.py в каждом приложении и запускает тесты в этом файле. Если в проекте много приложений, тесты запускаются по очереди.
Пора написать первый тест. Добавьте в файл posts/tests.py такой код:
Скопировать кодPYTHON
from django.test import TestCase
class TestStringMethods(TestCase):
def test_length(self):
self.assertEqual(len('yatube'), 6)
В командной строке запустите выполнение тестов:
Скопировать кодBASH
(venv) $ 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'...
Система обнаружила и запустила тест, он выполнился успешно.
Точка в выводе означает, что тест пройден успешно. При провале теста будет выведен символ F (Failed).
В больших проектах могут быть сотни или даже тысячи тестов, их совместный запуск отнимает много времени — до нескольких десятков минут. Если вы работаете над конкретной задачей, то не касающиеся её тесты отнимут время и не принесут пользы. Так что есть смысл запускать только часть тестов, указывая их адреса:
Скопировать кодBASH
(venv) $ python manage.py test posts
...skip...
(venv) $ python manage.py test posts.tests
...skip...
(venv) $ python manage.py test posts.tests.TestStringMethods
...skip...
(venv) $ python manage.py test posts.tests.TestStringMethods.test_length
...skip...
Дополнительные параметры запуска можно узнать, выполнив команду python manage.py test -h.
Терминология тестирования
Файл tests.py может содержать множество классов с методами-тестами. Такой файл называется «набор тестов» (на английском — test suite).
Каждый отдельный метод обычно тестирует работу какого-то одного логического элемента, фрагмента кода. По-английски такой кусочек кода называется test case, а на русском обычно хватает слова «тест».
Для тестирования бывают необходимы начальные данные, например — записи в базе или какой-то файл с исходными данными. Такие данные по-английски называются test fixtures; возможно, кто-нибудь и перевёл это выражение удачно, но в повседневной жизни это называют фикстурами.
Часть фреймворка тестирования, отвечающая за подготовку окружения и запуск тестов, по-английски называется test runner, но в русском языке эту часть обычно отдельно не выделяют.
Базовые предположения
Код тестов выглядит самобытно и непривычно, он не похож на знакомый вам код: вместо обычной логики тесты состоят из предположений (assertion), которые в ходе теста подтверждаются (в этом случае тест пройден) или опровергаются (тест провален).
Вернемся к тесту, который мы запускали:
Скопировать кодPYTHON
from django.test import TestCase
class TestStringMethods(TestCase):
def test_length(self):
self.assertEqual(len('yatube'), 6)
Выражение self.assertEqual(len('yatube'), 6) — ключевая строка теста, она проверяет предположение, что значение первого параметра эквивалентно второму параметру. Класс TestStringMethods унаследован от класса TestCase — это базовый класс для тестов в Django, он расширяет работу стандартного класса unittest.TestCase, добавляя к нему дополнительный набор предположений.
Вот доступный список простых методов-предположений:
assertEqual(a, b), проверка на эквивалентность. Проверяет, что a == bassertNotEqual(a, b), проверка на неравенство. То же что и a != bassertTrue(x), проверка на истину, bool(x) is TrueassertFalse(x), проверка на ложность, bool(x) is FalseassertRaises(), проверка, что метод порождает исключениеassertIs(a, b), проверка на тождественность, a is bassertIsNot(a, b), проверка на нетождественность, a is not bassertIsNone(x), проверка на тождественность None, x is NoneassertIsNotNone(x), проверка на нетождественность None, x is not NoneassertIn(a, b), проверка на вхождение в множество, a in bassertNotIn(a, b), проверка на невхождение в множество, a not in bassertIsInstance(a, b): является ли a экземпляром класса b, isinstance(a, b)assertNotIsInstance(a, b): не является ли a экземпляром класса b, not isinstance(a, b)
Каждому из этих методов можно передать параметр msg, он делает вывод более информативным. Измените набор тестов в файле tests.py так:
Скопировать кодPYTHON
from django.test import TestCase
class TestStringMethods(TestCase):
def test_length(self):
self.assertEqual(len('yatube'), 6)
def test_show_msg(self):
self.assertTrue(False, msg="Важная проверка на истинность")
И выполните его:
Скопировать кодBASH
(venv) $ python manage.py test posts
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F
======================================================================
FAIL: test_show_msg (posts.tests.TestStringMethods)
Traceback (most recent call last):
File "/Dev/Yatube/yatube/posts/tests.py", line 11, in test_show_msg
self.assertTrue(False, msg="Важная проверка на истинность")
AssertionError: False is not true : Важная проверка на истинность
Ran 2 tests in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
Предположение оказалось ложным, тест провален, а мы получили явный и читаемый сигнал о том, что ожидалось в этом тесте.
Расширенный набор предположений
Django расширяет список базовых assert-методов, добавляя специфические для web-разработки методы тестирования форм, ответов view-функций и классов.
В работе нам пригодятся такие методы:
assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='', fetch_redirect_response=True): проверка предположения, что ответ view-функции содержит редирект на нужный адрес, заодно можно проверить HTTP-код ответа страницы и код ответа адреса, на который ожидается редирект.assertURLEqual(url1, url2, msg_prefix=''): проверка предположения, что два адреса эквивалентны.
Например, /path/?x=1&y=2 — то же самое, что и /path/?y=2&x=1.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False): содержит ли ответ искомый текст, дополнительно можно проверить количество вхождений.assertNotContains(response, text, status_code=200, msg_prefix='', html=False): проверка предположения, что искомого текста нет в ответе view-функции.assertFormError(response, form, field, errors, msg_prefix=''): проверка на ошибки при валидации формы.assertTemplateUsed(response=None, template_name=None, msg_prefix='', count=None): проверка предположения, что определенный шаблон был применён для формирования ответа.assertTemplateNotUsed(response=None, template_name=None, msg_prefix=''): проверка предположения, что определенный шаблон не использовался для формирования ответа.assertHTMLEqual(html1, html2, msg=None): проверка предположения, что оба переданных HTML-документа одинаковы. При этом оба переданных HTML очищаются от пробельных символов, а порядок следования атрибутов в тегах не считается отличием.assertHTMLNotEqual(html1, html2, msg=None): операция, обратная предыдущей: два HTML-документа сравниваются на неравенство с учетом таких же условий, как в предыдущем методе.
Расширенные методы работают достаточно интеллектуально. Вот пример из документации метода assertHTMLEqual: оба предложенных варианта сравнения не вызовут ошибки, тест будет пройден, хотя, на первый взгляд, сравниваемые фрагменты кода заметно отличаются.
Скопировать кодPYTHON
self.assertHTMLEqual(
'<p>Hello <b>'world'!</p>',
'''<p>
Hello <b>'world'! </b>
</p>'''
)
self.assertHTMLEqual(
'<input type="checkbox" checked="checked" id="id_accept_terms" />',
'<input id="id_accept_terms" type="checkbox" checked>'
)