Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Python
Code

Как писать профессиональные модульные тесты на Python

by
Difficulty:IntermediateLength:LongLanguages:

Russian (Pусский) translation by Alex Grigorovich (you can also view the original English article)

Тестирование, это основа серьёзной разработки программного обеспечения. Существует много видов тестирования, но наиболее важный вид, это модульное тестирование. Модульное тестирование даёт уверенность в том, что вы сможете использовать хорошо протестированные блоки в качестве базовых элементов, полагаться на них и использовать при создании программы. Они увеличивают ваш инструментарий из проверенного кода за пределами ваших конструкций и стандартной библиотеки. Кроме того Python предоставляет отличную поддержку для написания модульных тестов.

Действующий пример

Прежде чем погрузиться в принципы, эвристики и руководства, давайте посмотрим репрезентативный модульный тест в действии. Класс SelfDrivingCar это частичное выполнение логики вождения автопилота автомобиля. Главным образом он контролирует скорость автомобиля. Он рспознаёт объекты впереди, ограничение скорости, а также прибытие или нет в пункт назначения.

Вот модульный тест для метода stop() чтобы раззадорить ваш аппетит. Я расскажу подробности позже.

Руководство по модульному тестированию

Основные идеи

Написание хороших модульных тестов это тяжелый труд. Написание модульных тестов занимает время. Когда вы меняете код, необходимо изменять тесты. Иногда в вашем тесте будут ошибки. Это означает, что вы должны быть по-настоящему идейным. Польза огромна, даже для небольших проектов, но это не бесплатно.

Будьте дисциплинированы

Вы должны быть дисциплинированным. Будьте последовательным. Убедитесь, что все тесты выполнены. Не отказывайтесь от тестов, только потому что вы «знаете», что код в порядке.

Автоматизируйте

Чтобы помочь вам быть дисциплинированным, необходимо автоматизировать модульные тесты. Тесты должны запускаться автоматически на значимых этапах, таких как проектирование или развертывание. В идеале ваша система управления версиями должна отклонять код, который не прошел все тесты.

Непротестированный код плохой по определению

Если вы не проверили его, вы не сможете сказать, что он работает. Это значит, что вы должны рассматривать его как плохой. Если это критический код, не разворачивайте его в производство.

Пояснения

Что такое модуль?

Модуль в смысле тестирования это файл, содержащий набор определённых функций или класс. Если у вас есть файл с несколькими классами, вы должны написать модульный тест для каждого из них.

Делать TDD или не делать TDD

Тест драйв разработка, это практика, где вы пишете тесты, до того как вы пишете код. Есть несколько преимуществ этого подхода, но я рекомендую отказаться от него, если у вас есть возможность написать тесты позже.

Причина заключается в том, что я проектирую код. Я пишу код, смотрю на него, переписываю, еще раз смотрю и быстро переписываю ещё раз. Написание тестов сначала ограничивает и замедляет меня.

После того, как я сделаю первоначальный дизайн, я сразу напишу тесты, до интеграции со всей системой. Тем не менее, это отличный способ найти себя в создании модульных тестов, и это гарантирует, что весь ваш код будет проверен.

Unittest модуль

Модуль Unittest поставляется со стандартной библиотекой Python. Она предоставляет собой класс под названием TestCase, из которого можно вызвать ваш класс. Затем можно переопределить метод setUp() чтобы подготовить среду до начала тестирования и/или метод класса classSetUp() чтобы подготовить среду для всех тестов (не очищающуюся между разными тестами). Существуют соответствующие методы tearDown() и classTearDown(), которые также можно переопределить.

Ниже приведены соответствующие разделы из нашего класса SelfDrivingCarTest. Я использую только метод setUp(). Я создаю новый экземпляр SelfDrivingCar и сохраняю его в self.car, поэтому он доступен для каждого теста.

Следующий шаг — написать специфические методы теста для тестирования кода внутри теста — в этом случае класс SelfDrivingCar — делает то, что он должен делать. Структура тестового метода довольно обычная:

  • Подготовка среды (необязательно).
  • Подготовьте ожидаемый результат.
  • Вызовите код теста.
  • Убедитесь, что фактический результат совпадает с ожидаемым результатом.

Обратите внимание, что результат не должен быть результатом метода. Он может быть изменением состояния класса, сторонним эффектом, например как добавление новой строки в базе данных, записью файла или отправкой сообщения по электронной почте.

Например метод stop() класса SelfDrivingCar не возвращает ничего, но он меняет внутреннее состояние, устанавливая скорость на 0. Метод assertEqual(), предоставляемый базовым классом TestCase используется здесь для проверки, того что вызов stop() работает, как и требуется.

Здесь на самом деле два теста. Первый тест, чтобы убедиться, что если скорость автомобиля равна 5 и stop() вызывается, то скорость становится равна 0. И еще один тест, чтобы убедиться, что ничего не случится, если вызвать stop() снова, когда автомобиль уже остановился.

Позже я расскажу о других тестах для дополнительных функциональных возможностей.

Doctest модуль

Doctest модуль очень интерестный. Он позволяет использовать интерактивные примеры в docstring и проверять результаты, включая исключения.

Я не применяют и не рекомендую использовать doctest для крупномасштабных систем. Хорошее тестирование требует много труда. Тест-код обычно намного больше, чем тестируемый код. Docstrings - это не совсем подходящий инструмент для написания комплексных тестов. Хотя они классные Вот как выглядит factorial функция для doc-тестов:

Как вы видите, docstring намного больше, чем код функции. Это не улучшает читаемость кода.

Запуск тестов

OK. Вы написали модульные тесты. Для большой системы у вас будет десятки / сотни / тысячи модулей и классов, возможно,размещенных в разных папках. Как вы будете запускаете все тесты?

Модуль unittest дает различные возможности для проведения групповых тестов и их программирования. Проверка Загрузки и Выполнения тестов /Loading and Running Tests. Но самый простой способ - открытие теста. Данный параметр был добавлен только в Python 2.7. В Pre-2.7 вы могли использовать nose, чтобы найти и запустить тесты. У nose есть несколько других преимуществ, таких как запуск тестовых функций без необходимости создания класса для ваших тестовых случаев. Но для целей в этой статьи, давайте придерживаться unittest.

Чтобы найти и запустить тесты на основе unittest, просто введите в командной строке:

python -m unittest discover

Unittest будет проверять все файлы и подкаталоги, запускать все найденные тесты и обеспечит хороший отчет, а также покажет время выполнения. Если вы хотите увидеть, какие тесты выполняются, вы можете добавить флаг -v:

python -m unittest discover -v

Существует несколько флагов, которые управляют операцией:

Определение степени покрытия кода

Определение степени покрытия кода часто игнорируют. Само понятие означает, сколько кода действительно проверено тестами. Например, если у вас есть функция с инструкцией if-else, и вы проверяете только ветвьif, то вы не знаете, работает ли ветка else или нет.  В следующем примере кода функция add() проверяет тип своих аргументов. Если оба являются целыми числами, они просто добавляют их. 

Если оба являются строками, то он пытается преобразовать их в целые числа и добавить. В противном случае он вызывает исключение. Функция test_add() проверяет функцию add() с аргументами, которые являются как целыми числами, так и аргументами, которые являются плавающим значением и проверяют правильное поведение в каждом случае. Но степень покрытия теста является незавершенной. В случае, если строковые аргументы были не протестированы. В результате тест проходит успешно, но ошибка в ветке, где аргументы были строками, не были обнаружены (см. «intg»?).

Вот вывод:

Практические Unit Tests

Написание тестов промышленной мощности нелегкая и непростая задача. Есть несколько вещей, которые нужно учитывать и компромиссы, на которые следует пойти.

Разработка тестирования

Если ваш код - это мешанина или набор нелепых строк, где разные уровни абстракции смешиваются друг с другом, и каждый фрагмент кода зависит от другого кода, вам будет трудно его протестировать.  Кроме того, всякий раз, когда вы что-то будете менять, вам также придется обновить кучу тестов.

Хорошей новостью является то, что разработка программного обеспечения общего назначения - это именно то, что вам нужно для проверки.  В частности, хорошо продуманный модульный код, в котором каждый компонент несет четкую ответственность и взаимодействует с другими компонентами через четко определенные интерфейсы, создаст удобные модульные тесты. 

Например, наш класс SelfDrivingCar отвечает за высокоуровневую работу автомобиля: ехать, останавливаться, перемещаться.  Он имеет метод calculate_distance_to_object_in_front(), который еще не реализован. Эта функциональность, вероятно, должна быть реализована полностью отдельной подсистемой.  Он может включать в себя считывание данных с различных датчиков, взаимодействие с другими автомобилями с помощью самостоятельного управления, целый стек "машинного зрения" для анализа изображений с нескольких камер.

Давайте посмотрим, как это работает на практике.  SelfDrivingCar примет аргумент, называемый object_detector, который имеет метод calculate_distance_to_object_in_front(), и он делегирует эту функцию объекту. Теперь нет необходимости в проверке, потому что object_detector отвечает (и должен быть протестирован) за него. Вам по-прежнему нужно, чтобы модуль тестировал то, что вы правильно используете object_detector.

Затраты и выгоды

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

Например, наш класс автопилота автомобилей критически важен. Если метод stop() не работает должным образом, наш автомобиль может убить людей, уничтожить имущество и пустить под откос другие автомобили. Если вы создаете автопилот для автомобиль, я подозреваю, предполагаю, что ваши юнит-тесты для метода stop() будут более тщательными, чем мои.

С другой стороны, если одна кнопка в вашем веб-приложении на странице, которая расположена тремя уровнями ниже главной страницы, немного мерцает, когда кто-то ее щелкает, вы можете это исправить, но, вероятно, вы не будете добавлять отдельный модульный тест для этого случая. Данный метод не оправдывает себя экономически.

Мышление тестирования

Мышление тестирования очень важно. Один из принципов, который я использую, состоит в том, что каждый кусок кода имеет как минимум двух пользователей: код, который использует его, и пользователь, который его тестирует. Это простое правило помогает с разработкой и зависимостей. Если вы помните, что вам нужно написать тест для своего кода, вы не добавите много зависимостей, которые трудно восстановить во время тестирования.

Например, предположим, что ваш код должен что-то вычислять. Для этого ему необходимо загрузить данные из базы, прочитать файл конфигурации и динамически обратиться к некоторыми REST API для получения актуальной информации. Все это может потребоваться по разным причинам, но при этом, ели вы разместите всё это в одной функции, это очень затруднит тестирование. Это возможно звучит смешно, но гораздо лучше изначально структурировать ваш код.

Чистые функции

Самым простым кодом для тестирования являются чистые функции. Чистые функции - это функции, которые имеют доступ только к значениям их параметров, не имеют побочных эффектов и возвращают один и тот же результат при вызове с теми же аргументами. Они не изменяют состояние вашей программы, не получают доступа к файловой системе или сети. Их преимуществ слишком много, чтобы их сейчас перечислять.

Почему их легко проверить? Потому что нет необходимости устанавливать специальную среду для их тестирования. Вы просто передаете аргументы и проверяете результат. Вы также знаете, что до тех пор, пока тестируемый код не изменится, ваш тест тоже не должен изменяться.

Сравните его с функцией, которая считывает XML-файл конфигурации. Ваш тест должен будет создать XML-файл и передать его имя файла в тестируемый код. Не трудная задача. Но предположим, что кто-то решил, что XML просто ужасен, и все файлы конфигурации должны находиться в JSON. Они занимаются своим делом и конвертируют все файлы конфигурации в JSON. Они проводят все тесты, включая ваши, и все они успешно завершаются!

Почему? Потому что код не изменился. Он все еще ожидает файл конфигурации XML, и ваш тест по-прежнему создает XML-файл для него.  Но при выполнении ваш код получит файл JSON, который он не сможет проанализировать.

Обработка ошибок тестирования

Обработка ошибок - это еще одна вещь, которая важна для тестирования. Она также является частью разработки. Кто несет ответственность за правильность ввода? Каждая функция и метод должны быть понятны. Если это ответственность функции, то она должна проверять вводные данные, но если это ответственность клиента, то функция выполняется, предполагая, что вводные данные правильные. Общая точность системы будет обеспечена путем тестирования для клиента, для того, чтобы убедиться, что он передает правильные данные в вашу функцию.

Как правило, вы хотите проверить ввод в общедоступном интерфейсе, потому что вам не обязательно знаеть, кто будет обращаться к вашему коду. Давайте посмотрим на метод drive() автопилота автомобиля. Этот метод ожидает параметр "destination". Параметр «destination» будет использоваться позже в навигации, но метод "drive" ничего не делает, чтобы мы смогли убедиться в его корректности.

Предположим, что цель должна быть параметрами широты и долготы. Существуют всевозможные тесты, которые можно выполнить, чтобы убедиться, что они действуют(например, определить пункт назначения в середине моря). Для наших целей давайте просто убедимся, что это параметры в диапазоне от 0,0 до 90,0 по широте и от -180,0 до 180,0 по долготе.

Вот обновленный класс SelfDrivingCar. Я просто выполнил некоторые из нереализованных методов, потому что метод drive() вызывает некоторые из этих методов прямо или косвенно.

Чтобы проверить, как обработались ошибки в тесте, я передаю неверные аргументы и думаю, что они должным образом будут отвергнуты. Вы можете сделать это, используя self.assertRaises() метод unittest.TestCase. Этот метод очень удачный, если код под тестированием действительно вызывает исключение.

Давайте посмотрим на это в действии. Метод test_drive() пропускает широту и долготу вне допустимого диапазона и ждет, что метод drive() вызовет исключение.

Тест не выполняется, поскольку метод drive () не проверяет его аргументы на достоверность и не вызывает исключения. Вы получите хороший отчет с полной информацией о том, что не удалось, где и почему.

Чтобы исправить это, давайте обновим метод drive(), чтобы фактически проверить диапазон его аргументов:

Теперь все тесты проходят.

Тестирование частных методов

Должны ли вы проверять каждую функцию и метод? В частности, следует ли тестировать частные методы, называемые только вашим кодом?  Обычный неудовлетворительный ответ: «В зависимости от ситуации».

Я постараюсь помочь вам и рассказать, от чего это зависит. Вы точно знаете, кто называет ваш частный метод - это ваш собственный код. В том случае, если ваши тесты служат для общедоступных методов, которые вызывают ваш частный метод и являются комплексными, которые вы уже полностью протестировали. Но если частный метод очень сложный, вы можете протестировать его отдельно. Решайте сами.

Как организовать ваши модульные тесты

В большой системе не всегда ясно, как провести тесты. Должен ли быть один большой файл со всеми тестами для пакета или отдельный тестовый файл для каждого класса? Должны ли тесты быть в том же файле, что и тестируемый код, или в том же каталоге?

Вот система, которую я использую. Тесты должны быть полностью отделены от тестируемого кода (следовательно, я не использую doctest). В идеале ваш код должен быть в пакете. Тесты для каждого пакета должны находиться в каталоге вашего дочернего узла. В каталоге тестов должен быть один файл для каждого модуля вашего пакета с именем test_<module name>.

Например, если в вашем пакете есть три модуля: module_1.py, module_2.py и module_3.py, вы должны иметь три тестовых файла: test_module_1.py, test_module_2.py и test_module_3.py в каталоге тестов.

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

Заключение

Юнит- тесты являются основой качествнного кода. В этом уроке я изучил некоторые принципы и рекомендации для работы с модульным тестированием и объяснил нескольких лучших практик. Чем больше система, которую вы строите, тем более важными становятся модульные тесты.  Но одних модульных тестов недостаточно. Другие типы тестов также необходимы для крупномасштабных систем: интеграционные тесты, тесты производительности, тесты нагрузки, тестирование на уязвимость, тесты на прием данных и другие.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.