Статьи

Автоматизированное интеграционное тестирование

В этой статье я пытаюсь обобщить мой опыт тестирования интеграции. Я расскажу о следующих темах:

  • Автоматический запуск интеграционных тестов и избежание ошибок, допущенных человеком.

  • Сделать результаты теста повторяемыми.

  • Выполнение тестов в системе в различных условиях.

  • Получение исчерпывающих результатов испытаний, пригодных для дальнейшего анализа.

  • Разработайте прочный испытательный стенд, который восстанавливается до чистого состояния.

Тестируемая система

Сначала рассмотрим предмет интеграционного тестирования — тестируемую систему. Наша команда занимается системами поддержки операций (OSS) для телекоммуникаций.

Решение OSS обычно состоит из нескольких приложений, работающих на разных компьютерах; каждое приложение предоставляет подмножество некоторых доменных функций. Например, система управления ошибками (FMS) объединяет сигналы неисправностей от нескольких систем управления элементами (EMS), затем экспертная система анализирует эти сигналы и принимает решение о выдаче заявки на устранение неисправностей в системе регистрации неисправностей (TTS).

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

Читатель, знакомый с сегодняшними практиками непрерывной интеграции (CI), может сказать: «Эй, парень, разделите свое решение на атомарные микросервисы, упакуйте эти сервисы в контейнеры, управляйте ими с помощью любого инструмента оркестровки, мгновенно разверните все в облаке, независимо для каждого выполнения теста … «Если это ваш случай, значит, вы достаточно счастливы в своей жизни, и эта статья для вас бесполезна. Эта статья о решениях, построенных на унаследованных приложениях, о старом и спагетти-разработанном программном обеспечении. Я принимаю на себя задачу применения методов КИ для интеграционного тестирования таких решений.

Модель испытательного стенда

Для простоты я рассмотрю простую модель, а не реальное решение. Давайте иметь модель, состоящую из трех компонентов:

  1. Производитель данных

  2. Система обмена сообщениями

  3. Потребитель данных

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

Результат, которого мы хотим

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

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

Второй вид результатов испытаний — это показатели производительности. Такие показатели могут включать

  • Статистика использования процессора и памяти,

  • Статистика чтения / записи диска,

  • Статистика отправки / получения по сети.

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

  • Сквозная задержка сообщения — время передачи сообщения от источника к потребителю,

  • Задержка доставки — как долго сообщение остается в Системе сообщений, ожидая потребления,

  • Отправить оценку для продюсера,

  • Получите тариф для потребителя,

  • Отправить статистику размера очереди для производителя,

  • Обработка статистики размера очереди для потребителя,

  • Статистика размера сообщения.

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

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

Кнопка сброса для тестового стенда

Испытательный стенд должен иметь одну обязательную функцию:  кнопку « Сброс» , то есть то, что переводит испытательный стенд в исходное, чистое и правильное состояние — из любого состояния. Эта функция жизненно важна для получения повторяемых результатов теста. Какая бы неправильная ситуация не возникала во время тестов, перед следующим выполнением текста мы просто нажимаем Reset. Следовательно, каждое выполнение теста начинается на чистом испытательном стенде, на которое не влияли предыдущие тесты.

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

Горькая правда в том, что мы должны это сделать. Мы должны разработать какую-то волшебную машину, которая обходит все ловушки и воскрешает систему из пепла. Для этого нам нужно изучить все входы и выходы каждого компонента системы.

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

Как мы можем реализовать кнопку Reset для нашей модели испытательного стенда? Позвольте мне выразить это в Groovy-подобном псевдокоде:

// Stop
atHost 'host1', { stopProducer }
atHost 'host3', { stopConsumer }
atHost 'host2', { stopMessagingSystem }
// Uninstall
atHost 'host1', { uninstallProducer }
atHost 'host3', { uninstallConsumer }
atHost 'host2', { uninstallMessagingSystem }
// Install
atHost 'host2', { installMessagingSystem }
atHost 'host3', { installConsumer }
atHost 'host1', { installProducer }
// Start
atHost 'host2', { startMessagingSystem }
atHost 'host3', { startConsumer }
atHost 'host1', { startProducer }

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

Выполнение тестов

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

  1. [host1] Сконфигурируйте Producer для отправки 10 сообщений размером 1 КБ каждую секунду.

  2. [host2] Настройте систему обмена сообщениями на использование буфера сообщений размером 1 МБ.

  3. [host3] Настройте приемника для получения сообщений с буфером 1 МБ.

  4. [host3] Настройте приемника для выполнения обогащения сообщения.

  5. [host1] Отправка сообщений в течение 1 часа.

  6. [все хосты] Соберите результаты тестов.

  7. [все хосты] Уборка.

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

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

В нашей команде работает Ansible — простой, но мощный механизм автоматизации. Мы описываем наш испытательный стенд с точки зрения инвентаря Ansible. Мы кодируем тестовые сценарии как Ansible playbooks. Давайте посмотрим, как это может быть для сценария, описанного в начале этого раздела.

Описание тестового стенда — файл инвентаризации Ansible, test_bed_inventory:

[producer]
host1

[messaging_system]
host2

[consumer]
host3

Тестовый сценарий — Ansible playbook. Это файл YAML, test-scene.yml:

---

- hosts: producer
  roles:
  - configure_producer

- hosts: messaging_system
  roles:
  - configure_messagingsystem

- hosts: consumer
  roles:
  - configure_consumer

- hosts: producer
  roles:
  - send_messages

- hosts: all
  roles:
  - collect_results

- hosts: all
  roles:
  - cleanup

Запустите сценарий с параметрами:

ansible-playbook -i test_bed_inventory \
  -e producer_message_rate=10 \
  -e producer_message_size_kb=1 \
  -e message_buffer_size_kb=1024 \
  -e consumer_enrichment=true \
  -e test_interval_min=60 \
  test-scenario.yml

Мы указываем параметры тестирования в качестве аргументов командной строки. Ansible передает их в роли, которые составляют книгу. Это извлечение тестовых параметров важно; позже мы увидим, как это помогает нам в тестировании в различных условиях.

Помимо автоматического выполнения на многих хостах, этот подход имеет следующие преимущества:

  • Описание испытательного стенда отделено от тестовых сценариев. Мы можем легко переключиться на другой испытательный стенд, просто предоставив другой файл инвентаря.

  • Конфигурация в виде кода: описание тестового стенда, тестовые сценарии, параметры теста, настройки приложения — все хранится в текстовых файлах под контролем исходного кода.

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

  • Хорошая отправная точка для дальнейшей автоматизации — смотрите позже.

Тестирование в различных условиях

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

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

Что ж, сейчас самое время нанять Дженкинса . Эта мощная система CI предоставляет плагин Matrix Project, который фактически реализует матрицу конфигурации. Давайте создадим матричное задание TestScenario1, которое будет запускать наш тестовый сценарий.

Для того , чтобы составить эту матрицу, я создал два определяемые пользователем оси: producer_message_rate и message_size . Каждая ось объявляет набор из трех параметров. После запуска задания Jenkins выполнит тестовый сценарий для каждой ячейки в матрице.

Единственное, чего не хватает, — это как подключить матрицу конфигурации Jenkins к книге воспроизведения Ansible, реализуя сценарий тестирования. Ansible Plugin Дженкинса  — именно то, что нам нужно. С помощью этого плагина мы вызываем наш сценарий playbook, передавая параметры из матрицы конфигурации в качестве параметров playbook.

Дженкинс — это больше, чем просто средство запуска тестовых сценариев. Дополнительные преимущества использования Jenkins:

  • Видимость процесса тестирования. Каждый может увидеть, как проходят тесты, каков текущий статус, каковы условия тестирования и так далее.

  • Отслеживаемость инструментов, используемых в тестах. Например, можно настроить задание на выполнение теста, чтобы получить некоторую конкретную версию симулятора или инструмента мониторинга. Можно отслеживать артефакты по отпечаткам пальцев MD5.

  • Хранилище результатов испытаний. Дженкинс имеет возможность архивировать артефакты после выполнения работы. Просто настройте архивирование результатов теста — файлов журнала, метрических файлов — и у вас будет доступ к результатам каждого выполнения теста для каждой конфигурации теста. Кроме того, Jenkins предоставляет REST API для захвата артефактов; эта функция очень полезна для автоматической обработки результатов.

  • Графический интерфейс — да, лучше нажимать кнопки, чем вводить команды оболочки!

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

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

Обработка исключительных случаев

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

Давайте подробнее рассмотрим книгу, в которой реализован наш упрощенный сценарий тестирования. Предположим, что Producer завершает работу с ошибкой и этот шаг в playbook завершается неудачно:

- hosts: producer
  roles:
  - send_messages

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

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

В нашей команде мы обрабатываем ошибки, используя Jenkins Pipelines . Конвейер Jenkins описывает рабочий процесс на языке конкретного домена (DSL) Pipeline. При запуске сборка конвейера опрашивает систему управления исходным кодом Jenkinsfile с определением конвейера. Конвейерная DSL на самом деле является расширением языка Groovy; он предоставляет синтаксические конструкции для использования большинства плагинов и функций Jenkins.

Вот эскиз конвейера для нашего тестового сценария:

try {
    // Run test scenario
    ansiblePlaybook extras: "… test parameters …",
                                    inventory: "test_bed_inventory",
                                    playbook: "test-scenario.yml"
} finally {
    try {
        // Either success or failure - collect test results
        ansiblePlaybook extras: "… test parameters …",
                                        inventory: "test_bed_inventory",
                                        playbook: "collect-results.yml"
    } finally {
        // In any case, cleanup the test bed
        ansiblePlaybook extras: "… test parameters …",
                                        inventory: "test_bed_inventory",
                                        playbook: "test-bed-cleanup.yml"
    }
}

Обратите внимание, что теперь у нас есть три отдельных сборника игр Ansible:

  • Чтобы запустить тестовый сценарий — test-scene.yml

  • Чтобы собрать результаты теста — collect-results.yml

  • Очистить испытательный стенд — test-bed-cleanup.yml

Плейбук test-scenery.yml не ограничивается этапами тестирования. Скорее всего, этот сборник также содержит

  • Проверка работоспособности кровати,

  • Задачи развертывания для различных инструментов тестирования, таких как симуляторы и мониторы,

  • Задачи подготовки, такие как настройка конфигурации в зависимости от условий тестирования.

Playbook collect-results.yml содержит задания для сбора всех интересных тестовых артефактов со всех машин тестового стенда.

Плейбук test-bed-cleanup.yml делает жизненно важную работу — он возвращает тестовый стенд в исходное состояние. Этот сборник отвечает за повторяемость и точность результатов испытаний. Для некоторых систем чистая книга воспроизведения может просто ярлык для кнопки сброса. Для других систем полный сброс может быть слишком долгой операцией, а чистая книга воспроизведения может иметь небольшую реализацию. В любом случае, он должен выполнять свою работу: обеспечить чистоту испытательного стенда.

Преодоление трудностей

В настоящее время не все необходимые нам функции доступны в используемых нами плагинах Jenkins. Самое обескураживающее в том, что у Дженкинса не может быть матричного конвейера. Если у меня есть набор тестовых конфигураций 3×3, я хочу запустить каждую из 9 конфигураций независимо, с правильной обработкой исключений. К сожалению, конвейерная работа не может быть матричной.

Мы преодолеваем эту проблему, используя параметризованные задания. Например, объявите два параметра:

  • providerMessageRates = «10 100 1000»

  • messageSizes = «10 100 1000»

В Jenkinsfile итерируйте все значения для обоих параметров:

for (rate in producerMessageRates) {
    for (size in messageSizes) {
    }
}

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

Следуя этому пути, мы сталкиваемся с другой проблемой: если какая-то тестовая конфигурация не проходит, последующие тестовые конфигурации даже не запускаются. Конечно, мы можем заключить каждую конфигурацию в конструкцию try-catch Groovy, поэтому следующая конфигурация будет работать, даже если предыдущая не удалась. Однако в этом случае сбои теста не видны — все задание сообщает об «успешном» состоянии, несмотря на то, что некоторые тесты не пройдены. Тестирование должно сделать видимые сбои, недостаток видимости является основным недостатком.

Наконец, мы нашли обходной путь: используйте параллельную конструкцию синтаксиса Jenkinsfile, но ограничьте число подчиненных узлов Jenkins ровно двумя. С более чем двумя узлами Jenkins будет пытаться запустить более одной тестовой конфигурации параллельно; однако мы не можем допустить этого, потому что у нас есть только один испытательный стенд. С одним узлом этот механизм просто не работает. Так много хаков просто для подражания плагину Matrix!

Вишня на этом торте: вы получите классический тупик, если начнете два конвейерных задания с двумя подчиненными узлами. Следи за своими шагами.

Синтаксис Jenkinsfile, хотя и происходит от Groovy, но все еще не имеет всех возможностей Groovy. Один из основных недостатков, на мой взгляд, заключается в том, что нельзя использовать Closures в Jenkinsfiles. Вот почему Jenkinsfiles иногда выглядит громоздким и страшным.

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

Что дальше?

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

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

Я не рассматриваю один очень важный аспект тестирования: обработку результатов тестирования. Прямо сейчас мы анализируем файлы результатов теста вручную с минимальной автоматизацией. Тем не менее, наш испытательный стенд создает сотни файлов журналов и метрик. Ручной анализ становится все более и более сложным. Более того, ручной анализ подвержен ошибкам — люди делают ошибки, это неизбежно. Это может быть грустной историей — иметь идеальный испытательный стенд, но давать неправильные выводы только из-за ошибки во время анализа результатов.

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