Мой опыт использования разных подходов к тестированию

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

Pytest сделал наборы тестов на Python более удобочитаемыми, гибкими и идиоматичными для самого языка. Тем не менее, есть много проектов, в которых используются nose или unittest, или их комбинация. Я хотел бы поделиться своим опытом работы с Pytest и рассказать, как работа с устаревшим сочетанием nose и unittest заставила меня еще больше оценить фреймворк.

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

Дело 1

У первой компании была центральная политика - все тщательно проверять. Тесты были более или менее организованы в виде модульных и интеграционных тестов. Команда QA отвечала за сквозные. Основным правилом было достижение 100% тестового покрытия. Код не был объединен, пока он не удовлетворял этому требованию.

В качестве тестовой среды мы использовали Pytest. Политика компании настоятельно рекомендовала мне ознакомиться с ней. Мне это, конечно, пошло на пользу. Но с другой стороны, та же политика породила некоторые более широкие проблемы в отношении методов тестирования, а именно:

  • Каждый фрагмент кода, даже тесты, являются частью проекта, который, в свою очередь, подлежит обслуживанию. Пытаясь охватить всю кодовую базу, мы стремимся тестировать тривиальный код. Код, который напрямую не связан с бизнес-требованиями, или код, который не играет решающей роли в системе. Подобные тесты приносят незначительную прибыль проекту, так как требуют дополнительной работы и снижают ремонтопригодность. Мы часто можем найти тривиальный код в функциях и модулях, называемых «утилиты» / «утилиты», то есть в небольших повторно используемых компонентах, которые служат для выполнения повторяющихся задач.
  • Некоторые программисты, вынужденные соблюдать правило полного покрытия кода, стремятся любой ценой придумывать тесты. Просто, чтобы он прошел и ударил по счетчику линий. В таких ситуациях мы попадаем в темный лес широко распространенных испытательных грехов. Чрезмерно смоделированные тесты, ложноположительные тесты, тесты, которые осведомлены о деталях реализации кода и т. Д. Само собой разумеется, что такого рода тесты не служат проекту. Они снижают качество проекта и не приносят никакой ценности.

Тем не менее, несмотря на жесткие правила компании, я получил большое уважение к понятию тестирования программного обеспечения. Я лучше спал после работы, будучи увереннее в написанном коде.

Дело # 2

У другой компании - стартапа - был немного другой подход. Более быстрый темп написания кода, несколько внутренних процедур и правил. Как говорится, «двигайся быстрее и ломай вещи». Я не осознавал, что не являюсь поклонником этой мысли, пока не столкнулся с ней напрямую.

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

Одна из дискуссий, которая мне запомнилась, заключалась в следующем: «Зачем беспокоиться о ваших тестах, если вы не можете предсказать все возможные входные / выходные данные?». Довольно точно, что мы не можем все предсказать. В конце концов, мы люди. Но данный вопрос сигнализировал мне, что с точки зрения этого конкретного разработчика нет никакой связи между набором тестов и тем, как он дает нам информацию о дизайне нашего кода. Речь идет не только о том, чтобы убедить себя в том, что проект соответствует бизнес-требованиям. Он также дает нам обратную связь о том, не является ли наша система запутанной или взаимосвязанной. Такое программное обеспечение обычно сложно тестировать.

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

Однако набор тестов, который мне удалось написать, также был написан на Pytest.

Возвращаемся к носу и unittest

Большую часть удовлетворения от тестирования я приписал самому тестированию. Как будто наличие набора тестов было некоторой формой автоматического самоудовлетворения. Я никогда не рассматривал альтернативы Python. Я воспринял Pytest - и то, что он предлагает - как должное.

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

Котельная плита

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

Рассмотрим простой сценарий. Одна из самых популярных функций для написания - когда вы начинаете свое приключение с программированием - это функция, определяющая, является ли год високосным. Пример следующий (я знаю о существующих встроенных calendar.isleap 😉):

Соответственно, unittest тест должен выглядеть так:

Здесь есть немного избыточного кода, поскольку мы должны написать специальный класс и унаследовать его от TestCase. Стоит отметить, что представленная функция - это простая, чистая функция, не требующая каких-либо подготовительных шагов (другими словами, фазы настройки / демонтажа). Чаще вы сталкиваетесь с более сложными случаями, когда необходимо подготовить окружение для правильного тестирования единицы поведения (например, загрузить конфигурацию системы или установить соединение с тестовой базой данных). Теперь с unittest мы объединяем этап настройки с классом, чтобы аналогичные тестовые примеры также могли наследовать от него. Это скользкая дорожка, которая связывает наши тестовые примеры воедино и заставляет разработчика знать обо всех этих классах и их, надеюсь, редких изменениях.

Pytest (и nose по сути) подходит к этой теме легче. Предлагается писать функции вместо классов. Вам следует использовать классы только в том случае, если вы хотите сгруппировать тесты, которые имеют общий контекст. Хотя это не требование сверху вниз. Посмотрим, как это выглядит:

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

Основное различие между nose и Pytest в этом примере - момент импорта. nose требуется импорт функции для обслуживания фактических результатов утверждения. Pytest использует уже существующий синтаксис Python, то есть ключевое слово assert, для достижения того же результата. nose требования к импорту и unittest наследованию подводят нас к другому вопросу.

Идиоматичность

Pytest идет еще дальше, избавляясь от когнитивного багажа разработчика. Одним из его сильных сторон является использование ключевого слова Python assert для сравнения выражений всех типов. Используя то, что уже предлагает ваш язык, вы поддерживаете определенный уровень идиоматической синергии между вашим решением и доменом (то есть языком Python), в котором оно работает. Вы избегаете повторного изобретения колеса.

Мы можем попрощаться с jUnit-подобным API TestCase - assertEqual, assertDictEqual и т. Д. И нас не заставляют импортировать утверждающие функции каждый раз, когда мы хотим выполнить простую операцию сравнения двух объектов. Это все уже есть. На ум приходит известный девиз Python:

Лучше простое, чем сложное

И вроде бы уважают.

Простота также отражается в описательных отчетах, которые генерирует Pytest. Давайте посмотрим, что nose возвращает при ошибке утверждения:

Помимо отчета, мы получаем много избыточной информации: локальные пути и небольшой стек вызовов внутренних функций nose. Вывод слишком подробный. Некоторые вещи не имеют отношения к читателю. Pytest справляется с этим лучше:

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

Что полезно, так это то, что разворачивание деталей словаря также было упрощено. Обычно, как в примере выше, словарь складывается. В _26 _ / _ 27_, чтобы развернуть словарь, мы должны были изменить сам код, установив TestCase атрибут класса maxDiff в значение Нет. В pytest мы можем достичь того же результата, просто используя флаг -v команды оболочки pytest. Это может показаться незначительной особенностью, но я ценю моменты, когда мне не нужно переключать контекст без явной причины.

Утилиты для тестирования

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

На протяжении всего моего опыта написания тестов я довольно часто сталкивался с этой потребностью. А в решениях, отличных от pytest, требование превращается в проблему. В unittest и nose нет готовых решений для параметризации тестов. Этого можно достичь с помощью nose, поскольку он позволяет использовать генераторы внутри тестовых функций. Но тогда мы не можем, например, наследуют наши классы тестов от unittest.TestCase. Это довольно неприятно, когда у вас есть сочетание классов в стиле unittest и nose.

Предположим, мы хотим параметризовать is_leap_year в nose. Структура параметризованных тестов в носу не впечатляет. Это будет выглядеть соответственно:

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

В pytest мы параметризуем следующий способ:

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

И это не единственная упрощающая утилита, которую вы можете использовать. Я часто использую две другие второстепенные утилиты:

  • approx - функция для утверждения чисел с плавающей запятой.
  • monkeypatch - приспособление (приспособления, обсуждаемые в разделе ниже) для быстрого изменения объекта, статического словаря или переменной окружения. Все изменения отменяются после тестирования.

Модульность

Наибольшее влияние на форму и структуру моих тестов оказала, на мой взгляд, самая заметная особенность Pytest: приспособления. Многоразовые функции, которые являются зависимостями, введенными в наши тесты.

Чтобы внедрить прибор, мы передаем его имя в список параметров желаемого теста. Светильники вызываются автоматически при обнаружении введенного имени. Их можно вызывать один раз за обнаружение теста, обнаружение тестового модуля или даже один раз за запуск набора тестов.

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

Хотелось бы иметь общий способ доставки строк на тесты. Приспособления здесь, чтобы помочь:

Теперь, чтобы использовать прибор в тесте, мы вводим его имя в параметры тестовой функции. Остальное сделает Pytest:

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

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

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

Стоит отметить, что светильники и параметризацию можно комбинировать. Учитывая наше предыдущее приспособление:

Он заставляет тестовые функции с использованием прибора запускаться X раз, в зависимости от длины параметров, включенных в прибор:

Сообщество

Последний факт, о котором стоит упомянуть, - который я не ценил до работы с _48 _ + _ 49_, - это поддержка сообщества pytest. nose активно не поддерживается. В мире не так много плагинов и расширений. Есть nose2 альтернатива, но, похоже, она не так активно разрабатывается, как pytest.

Используя pytest, вы получаете доступ ко множеству расширений. Расширения, которые обычно предоставляют новые функции через новые приспособления. Например, pytest-catchlog, чтобы подтвердить правильность ведения журнала в вашей системе. Или pytest-mock, чтобы использовать макеты через согласованный pytest-подобный интерфейс (он также обеспечивает фазу разрушения, что приятно).

Заключение

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

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

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

Pytest поддерживает выполнение устаревших тестов unittest (и в некоторой степени nose), поэтому возможен вариант последовательного перехода на Pytest. Это окупается!

Полезные ссылки

  1. Документация Pytest
  2. Тестирование Python с помощью pytest Брайана Оккена
  3. Подкаст Test and Code

Если вам понравился этот пост, нажмите кнопку хлопка ниже 👏👏👏

Вы также можете подписаться на нас в Facebook, Twitter и LinkedIn.