Python для продвинутых
Теория: Мир Python: тестирование с помощью unittest
Введение
Этим уроком открывается цикл практических занятий по написанию тестов с применением различных инструментов.
unittest - инструмент для тестирования в Python. Это стандартный модуль для написания юнит-тестов на Python. Unittest это порт JUnit с Java. Иными словами, и в коде модуля, и при написании тестов легко прослеживается ООП стиль, что весьма удобно для тестирования процедур и классов.
Документация доступна по следующим ссылкам: python3, python2
В данном инструменте много возможностей: проверки (assert*), декораторы, позволяющие пропустить отдельный тест (@skip, @skipIf) или обозначить сломанные тесты (@expectedFailure) и этим не заканчивается список. Использование assert'ов с лихвой покрывает нужды при написании тестов.
Описание unittest
Полезная черта unittest - автоматизированное тестирование. Есть и другие:
- можно собирать тесты в группы
- собирать результаты выполнения тестов (например, для отчета)
- ООП стиль позволяет уменьшить дублирование кода при схожих объектах тестирования
В использовании unittest присутствуют несколько концепций
test case
test case -это наименьшая единица тестирования. Он проверяет конкретный ответ для конкретного набора входных данных.
test suite
test suite представляет собой сборник тестовых случаев, тестовых наборов. Используется для агрегирования тестов, которые должны выполняться вместе.
test fixture
test fixture - это фиксированное состояние объектов используемых в качестве исходного при выполнении тестов.
Цель использования fixture - если у вас сложный test case, то подготовка нужного состояния легко может занимать много ресурсов (например, вы считаете функцию с определенной точностью и каждый следующий знак точности в расчетах занимает день). Используя fixture (на сленге - фикстуры) предварительную подготовку состояния пропускаем и сразу приступаем к тестированию.
Test fixture может выступать, например, в виде:
- состояние базы данных
- набор переменных среды
- набор файлов с необходимым содержанием.
test runner
test runner - это компонент, который организует выполнение тестов и предоставляет результат пользователю.
Рекомендации к написанию тестов
При написании тестов следует исходить из следующих принципов:
- Работа теста не должна зависеть от результатов работы других тестов.
- Тест должен использовать данные, специально для него подготовленные, и никакие другие.
- Тест не должен требовать ввода от пользователя
- Тесты не должны перекрывать друг друга (не надо писать одинаковые тесты 20 раз). Можно писать частично перекрывающие тесты.
- Нашел баг -> напиши тест
- Тесты надо поддерживать в рабочем состоянии
- Модульные тесты не должны проверять производительность сущности (класса, функции)
- Тесты должны проверять не только то, что сущность работает корректно на корректных данных, но и то что ведет себя адекватно при некорректных данных.
- Тесты надо запускать регулярно
Практика
К написанию тестов стоит относится также как и к основному коду.
Написание тестов является хорошей инвестицией в будущее программы:
- Когда ваша программа становится настолько большой, что не помещается целиком у вас в голове, то это отличный звоночек, что стоит покрывать все тестами
- Если сейчас ваша программа не испытывает проблем, то через какое-то время библиотеки, которые вы используете, могут начать обновляться без обратной совместимости. Вот здесь-то тесты помогут
- Когда вы занимаетесь рефакторингом кода - тесты помогут не сломать лишнего
Есть и другие причины писать тесты. В целом, практика показывает, что до тестов надо дорасти - в какой-то момент приходит понимание зачем же тратить на них время.
В качестве примеров использования unittest продемонстрирую и опишу основные возможности модуля. На мой взгляд это те 20% которые помогут сделать 80% результата.
Пример синтаксиса №1
Рассмотрим следующий код:
В данном примере показан общий шаблон для большинства тестов - здесь и наследование от TestCase, здесь и два простых теста, а также перегрузка встроенных в TestCase методов:
- Метод def setUp(self) вызывается ПЕРЕД каждым тестом.
- Метод def tearDown(self) вызывается ПОСЛЕ каждого теста
Список подобных готовых функций такой:
- setUp – подготовка прогона теста; вызывается перед каждым тестом.
- tearDown – вызывается после того, как тест был запущен и результат записан. Метод запускается даже в случае исключения (exception) в теле теста.
- setUpClass – метод вызывается перед запуском всех тестов класса.
- tearDownClass – вызывается после прогона всех тестов класса.
- setUpModule – вызывается перед запуском всех классов модуля.
- tearDownModule – вызывается после прогона всех тестов модуля.
Если запустить скрипт:
То получим:
Теперь мы знаем как писать тест, как запускать. Перейдем к освещению вариантов использования тестов.
Пример синтаксиса №2 - №...
Прямо в док-строках описал когда выбрасывают ошибки эти проверки. В примере показаны как новые названия, так и их старые аналоги (в старых проектах такие еще встречаются)
Практические примеры
Перейдем к более практическим примерам. На них и рассмотрим еще некоторые важные способности unittest
- Как тестировать конструктор?
- Как тестировать структуры данных?
- Как тестировать работу с БД?
Как тестировать конструктор?
Приведу пример, как можно протестировать конструктор
В коде видно, что есть класс, в конструкторе которого кидается исключение если значение аргумента не удовлетворяет условию. Это условие ловится функцией self.assertRaises или ее старым названием self.failUnlessRaises
Во втором примере показан тест на количество аргументов в классе.
Как тестировать структуру данных?
Продемонстрирую пример тестирования структуры данных. В данном случае есть класс Point, в котором хранится два float’а x,y. А дальше весь код такой же как и для других тестов.
Как тестировать работу с БД?
Примера под тестирование БД не приведу, однако, расскажу об общих соображениях.
Работа с базой не так проста, как с обычной функцией, ведь база - это не просто программный код, база данных - это объект, сохраняющий своё состояние. И если мы начнём в процессе тестирования изменять данные в базе, то после каждого теста база будет изменяться. Это может помешать последующим тестам и необратимо испортить базу данных.
Ключ к решению проблемы - транзакции. Одна из особенностей этого механизма состоит в том, что до тех пор пока транзакция не завершена, вы всегда можете отменить все изменения и вернуть базу в состояние на момент начала транзакции. Алгоритм такой:
- открываем транзакцию;
- если нужно, выполняем подготовительные действия для тестирования;
- выполняем модульный тест (или просто запускаем сценарий, работу которого хотим проверить);
- проверяем результат работы сценария;
- отменяем транзакцию, возвращая базу данных в исходное состояние.
Даже если в тестируемом коде останутся незакрытые транзакции, внешний ROLLBACK всё равно откатит все изменения корректно.
Помимо транзакций стоит обратить внимание, что тестирование следует делать на тестовой базе данных. Идеальным случаем будет полная инициализация окружения перед исполнением теста.
Выводы
В уроке описаны базовые элементы работы с unittest. Не были затронуты статусы тестов, отчеты о тестировании. Однако, данной теории хватит для написания тестов. Давайте это сейчас и проверим, переходите к практике.

