Статьи

Подробнее о модульном тестировании кукольного модуля

Ранее я делал презентации и публикации в блогах о Puppet и тестировании модулей — моя позиция заключается в том, что вы должны относиться к коду Puppet как к следующему: коду. Как и в случае с основными языками программирования, возможно (и хорошая практика) протестировать манифесты Puppet, чтобы у вас было больше уверенности в том, что они работают, когда придет время их запустить.

Есть некоторые другие факторы, которые играют роль:

  • Сделайте ваши модули универсальными. Любая специфика среды или хоста, которую вы включили в классы или определения, значительно затрудняет их тестирование в разнородной (читай: чистой) среде, такой как конвейер CI.
  • Как следствие к первому пункту, классы должны быть параметризованы так, чтобы их можно было использовать различными способами — в вашей производственной среде, в стадии подготовки, в QA, разработке (и т. Д.) И, конечно, в вашем тестовом конвейере.
  • Свободно соедините ваши модули. Тесные зависимости, обеспечиваемые строгими ограничениями порядка, означают, что вы не можете протестировать каждый класс отдельно, не включая все зависимости. Если говорить непосредственно из опыта, это может означать, что ошибки гораздо сложнее отследить, когда приходится искать в нескольких местах один неисправный ресурс.

Именно такие проблемы, как этот последний пункт, вызывают наибольшее горе при тестировании модулей Puppet в нашей среде. У нас есть коллекция общих модулей, называемых dist , которые предоставляют как функциональные возможности многократного использования, когда это требуется модулями приложения (например, возможность легко настроить сервер MySQL, стандартный способ обеспечения Apache / Nginx и т. Д.), Так и конфигурацию, которую мы ожидаем стандартизированы для всех машин — другими словами, платформы . На самом деле класс-оболочка, который использует стандартизированную конфигурацию, называется просто «платформа».

Платформа использует множество вспомогательных функций, которые прикладные модули могут воспринимать как должное. Примером является класс yum . Здесь мы устанавливаем стандартный файл /etc/yum.conf с некоторыми настраиваемыми значениями, каталог фрагментов /etc/yum.repos.d и набор стандартных фрагментов репозитория, таких как ОС, обновления и так далее. Модуль также включает в себя определенный тип, yum :: repo, который во многом похож на встроенную версию, но работает с классом yum, чтобы иметь полностью управляемый каталог фрагментов — если фрагмент yum находится на диске, но не управляется Puppet, он удален.

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

class foo {
  yum::repo { 'myapp':
    descr => 'Repository for My App',
    ...
  }
...
}

Чтобы проверить это, у вас должно быть следующее в каталоге tests:

class { 'foo': }

Это, конечно, тривиальный пример без параметров. Здесь у нас уже есть проблема при попытке модульного тестирования класса — он сразу же завершится неудачей из-за того, что класс yum не был создан в каталоге, таким образом, не удовлетворяя зависимости, которую имеет определенный тип yum :: repo . Обычно мы работаем над этим, просто добавляя его в тест:

class { 'yum': }
class { 'foo': }

 

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

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

  1. Это сильно унесет время компиляции (и, следовательно, тестирования). Обычно мы можем уйти с каждым тестом, выполняющимся в течение нескольких секунд, и полным модулем приложения (для всей работы), возможно, за 30-60 секунд. Потяните все платформы для каждого теста, и один модуль приложения займет несколько минут. Умножьте это на сотни модулей приложения, и вы ожидаете значительного увеличения времени тестирования.
  2. Это больше не действительно модульное тестирование. На данный момент мы проводим полномасштабное интеграционное тестирование. Не поймите меня неправильно — это тоже ценно, но для этого есть время и место, и я не хочу разрушать модульное тестирование, которое у нас уже есть, с неявной ограниченной областью действия (и, следовательно, более легким поиском неисправностей). ) что это обеспечивает.

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

define yum::repo (
  $descr,
  $baseurl,
  $enabled,
  ...
) { }
class { 'foo': }

 

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

  • Совершенно очевидно, что у языка Puppet просто нет возможностей для продвинутой насмешки (что неудивительно — это не его основная цель). Было бы интересно, если бы сторонняя библиотека предоставила Puppet макеты, хотя …
  • Теперь у нас есть несоответствие в наших методах тестирования. Единственный раз, когда вам нужно смоделировать класс / определение, это когда у него есть неразрешенные зависимости, о которых вам не нужно беспокоиться. Во всех других случаях мы все еще можем использовать реальную версию (которая будет находиться на пути к модулю, поскольку мы устанавливаем модули dist в виртуальную машину тестирования с помощью модуля приложения). Теперь есть два слегка запутанных механизма для тестирования.
  • Будет ли найденная версия найдена до реальной версии, которая находится в другом месте на модульном пути? Я не заглядывал в код, чтобы узнать, будет ли он найден сразу после его анализа, но мне не нравится неизвестный фактор.
  • Один из принципов многоразовой инкапсулированной функциональности, который мы предоставляем в нашем дистрибутивеМодули в том, что вам не нужно знать детали. Фактически, благодаря параметризованным классам, определенным типам, пользовательским типам и поставщикам часто невозможно определить, что встроено в Puppet и каков один из упомянутых ранее способов его расширения — и это именно так, как и должно быть. , Вы действительно хотите макетировать любой из этих типов ресурсов, когда он предоставляет гораздо больше, чем просто контейнер с параметрами? Проверка ввода, согласованность между входными параметрами, проверка платформы, встроенная обработка зависимостей между ресурсами одного типа — все это веские причины придерживаться того, что эти типы дают вам бесплатно. Кажется неправильным удалять их (что является эффективной причиной, по которой вы в первую очередь проводите тестирование компиляции).

Раньше я не тратил на это много времени, как на другие проблемы с куклами, потому что на данный момент (к счастью) это в основном всего лишь раздражение. Мы можем добавить отсутствующие тестовые зависимости вручную, и большинство наших пользователей становятся достаточно опытными, чтобы сделать это сами. Мне интересно, что сообщество думает об этой теме, и решили ли вы эту проблему самостоятельно? Пожалуйста, оставляйте комментарии; Я хотел бы знать, что вы думаете!