Теория

Запуск первых тестов

В мире 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 # Create your tests here. 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
# Запускаем все тесты приложения posts (venv) $ python manage.py test posts ...skip... # Запускаем все тесты приложения posts которые расположены в файле tests (venv) $ python manage.py test posts.tests ...skip... # Запускаем конкретный класс unit-тестов (venv) $ python manage.py test posts.tests.TestStringMethods ...skip... # Запускаем метод test_length() из класса TestStringMethods # из файла tests.py из директории posts (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
# Каждый логический набор тестов — это класс, # который наследуется от базового класса TestCase from django.test import TestCase # Каждый класс — это набор тестов. Имя такого класса принято начинать со слова Test. # В файле может быть множество наборов тестов, # не обязательно иметь один класс для всего приложения. class TestStringMethods(TestCase): # Каждый отдельный метод в наборе тестов должен начинаться со слова test # таких методов-тестов в наборе может быть множество. def test_length(self): # В этой строке находится собственно тест который проверяет # предположение (assertion) являются ли переданные параметры # эквивалентными (equal) self.assertEqual(len('yatube'), 6)
Выражение self.assertEqual(len('yatube'), 6) — ключевая строка теста, она проверяет предположение, что значение первого параметра эквивалентно второму параметру. Класс TestStringMethods унаследован от класса TestCase — это базовый класс для тестов в Django, он расширяет работу стандартного класса unittest.TestCase, добавляя к нему дополнительный набор предположений.
Вот доступный список простых методов-предположений:
  • assertEqual(a, b), проверка на эквивалентность. Проверяет, что a == b
  • assertNotEqual(a, b), проверка на неравенство. То же что и a != b
  • assertTrue(x), проверка на истину, bool(x) is True
  • assertFalse(x), проверка на ложность, bool(x) is False
  • assertRaises(), проверка, что метод порождает исключение
  • assertIs(a, b), проверка на тождественность, a is b
  • assertIsNot(a, b), проверка на нетождественность, a is not b
  • assertIsNone(x), проверка на тождественность None, x is None
  • assertIsNotNone(x), проверка на нетождественность None, x is not None
  • assertIn(a, b), проверка на вхождение в множество, a in b
  • assertNotIn(a, b), проверка на невхождение в множество, a not in b
  • assertIsInstance(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): # действительно ли первый аргумент — True? 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 # первый тест пройден (точка), второй тест провален (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 : Важная проверка на истинность # Нет, False — это не 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>&#x27;world&#x27;!</p>', '''<p> Hello <b>&#39;world&#39;! </b> </p>''' ) self.assertHTMLEqual( '<input type="checkbox" checked="checked" id="id_accept_terms" />', '<input id="id_accept_terms" type="checkbox" checked>' )
Задача
Напишите тесты для модуля, который переводит кириллический текст в сообщения на азбуке Морзе. Убедитесь, что библиотека умеет правильно обрабатывать множество потенциальных ситуаций:
  • Текст превращается в правильное сообщение
  • Если функция модуля получает текст не с кириллическими или латинскими символами, то они не преобразуются на выводе
  • Если функция модуля получает не текст, а какой-то другой объект, то она порождает правильно исключение
  • Проверка правильности указанного языка
Подсказка
Вам точно понадобятся методы assertEqual и assertIsInstance. Конструкция with self.assertRaises(ValueError): даст вам несколько раз вызвать тестируемую функцию, каждый раз с различными аргументами.
Код
Результат