2010 г.
Архитектура среды тестирования на основе моделей, построенная на базе компонентных технологий
Кулямин В. В.
Институт системного программирования РАН (ИСП РАН), Москва
Назад Содержание Вперёд 3. Архитектурный каркас для тестирования на основе моделей
В данном разделе описывает предлагаемый подход к построению компонентной архитектуры инструментов тестирования на основе моделей, удовлетворяющей сформулированным выше требованиям. Однако прежде стоит сделать ряд замечаний, касающихся выбираемых средств решения поставленных задач.
- Как отмечалось выше, необходимо использовать в моделях разные техники описания ограничений на поведение тестируемых компонентов. При этом нужно отдавать себе отчет в том, что поддержка совершенно произвольных видов моделей в рамках одной технологии, скорее всего, недостижима. Поэтому нужно сразу отметить ряд практически важных походов, совместная поддержка которых реализуема.
При моделировании программных интерфейсов с устоявшимися требованиями достаточно удобно применять контрактные спецификации в виде пред- и постусловий, опирающихся на модельное состояние моделируемых компонентов. Подходы на их основе продемонстрировали достаточную масштабируемость и эффективность в терминах трудозатрат на описание некоторого набора элементов интерфейса [38,50,51].
C другой стороны, для моделирования вычислений с плавающей точкой, сложных протоколов и ряда других видов ПО, иногда более удобно использовать операционные модели, являющиеся, по сути, альтернативными реализациями той же функциональности. Наиболее удобными на практике моделями такого вида оказываются расширенные автоматы и системы переходов с возможностью их композиции.
При моделировании некоторых реактивных систем, обрабатывающих большие потоки событий, или служб, предназначенных для регулярной обработки данных из большой базы, полезными оказываются потоковые контракты, описывающие ограничения не на конечный результат обработки, а на обработку одного структурного элемента во входном потоке данных
- Модели поведения и модели ситуаций, а также сами тесты и элементы инструментария разработки предполагается оформлять в виде компонентов или наборов компонентов (подсистем) в рамках выбранной базовой компонентной технологии, с минимальным добавлением каких-либо новых конструкций, требующих дополнительной языковой и инструментальной поддержки.
Такой подход позволит применять для работы с этими моделями и для их интеграции с проверяемыми компонентами все инструменты, средства и техники, предлагаемые базовой компонентной технологией. Соответственно, значительно снижаются затраты на сопровождение и развитие инструментария, поддерживающего такую технологию. Это обеспечивает возможность использования того же инструментария и созданных моделей компонентов при разработке тестов для подсистем и крупных систем. Тем самым создается основа для выполнения требований к компонентным технологиям верификации.
- Применение широко распространенных компонентных технологий и языков программирования позволяет снизить трудности обучения использованию инструментария, а также при необходимости использовать многочисленные вспомогательные библиотеки, разработанные для решения специфических задач модульного тестирования.
- Верификационные системы часто из-за более многочисленного ассортимента различных компонентов получаются сложнее систем, для проверки которых предназначены. Поэтому еще более важно снижать их сложность и трудоемкость их поддержки и развития. Для облегчения интеграции и переконфигурирования систем из многочисленных компонентов предлагается везде, где можно, использовать неинвазивные техники композиции и интеграции компонентов, избегающие внесения каких-либо изменений в их код. Это можно обеспечить за счет широкого использования образца внедрения зависимостей (dependency injection) [52] и поддерживающих его библиотек-контейнеров, а также других средств современных компонентных технологий.
Виды компонентов и их интеграция
Основой инструментария тестирования на основе моделей предлагается сделать контейнер внедрения зависимостей (dependency injection container), позволяющий задавать список компонентов, входящих в систему, инициализировать их состояние и определять связи между ними внешним образом, без вмешательства в код этих компонентов.
Верификационная система строится из компонентов различных типов.
- Собственно, проверяемые компоненты.
На них накладывается только одно ограничение: возможность их внешней инициализации с помощью контейнера внедрения зависимостей. В большинстве случаев это ограничение выполняется, иначе обычно достаточно просто написать компонент-обертку, удовлетворяющий ему и предоставляющий доступ к проверяемым операциям исходного компонента.
Проверяемые компоненты не имеют зависимостей от тестовой системы, за исключением заглушек, подменяющих необходимые им для работы компоненты.
- Модели поведения (обобщенные контракты).
Они оформляются на базовом языке программирования как классы с несколькими методами, выполняющими определенные роли. Например, если используется чисто декларативная спецификация, в ней должны быть определены пред- и постусловия, причем любой метод, возвращающий булевское значение, может играть эти роли. Для спецификации, использующей модельное состояние компонента, необходимо определить синхронизатор состояния, вызываемый, чтобы поддерживать в соответствии состояние модели и реальное состояние проверяемого компонента. Исполнимые спецификации должны определять предусловия и модельные операции.
Модели поведения зависят от проверяемых компонентов или, в случае существенных различий в интерфейсах между моделью и моделируемым компонентом — от адаптеров, устраняющих такие различия.
- Модели взаимодействия.
При описании многокомпонентных систем иногда, помимо моделей отдельных компонентов, необходимо явно вводить модель их взаимодействия, позволяющую оценить корректность сложных конгломератов воздействий и реакций, в которых задействовано несколько компонентов, каждый из которых осведомлен лишь о событиях, относящихся к его интерфейсу. Например, моделью взаимодействия является так называемая семантика чередования для асинхронных взаимодействий параллельно работающих компонентов, в рамках которой корректен любой набор событий, который можно линейно упорядочить так, чтобы каждое отдельное событие в таком порядке стало корректным относительно моделей компонентов, создающих или обрабатывающих его [50].
Модели взаимодействия оформляются в виде шаблонных библиотечных модулей, привязываемых в конфигурационном файле к соответствующим им группам компонентов. Для каждого конкретного взаимодействия порождается экземпляр такого шаблона, зависящий от моделей поведения вовлеченных в него компонентов.
- Модели ситуаций.
Модели ситуаций оформляются на базовом языке программирования в виде методов, фиксирующих наступление определенных ситуаций после проверки описывающих их ограничений. Модели ситуаций для некоторой операции могут включать как пре-ситуации, определяемые входными аргументами операции и состояниями вовлеченных компонентов до ее вызова, так и пост-ситуации, соответствующие определенным свойствам результатов и состояний компонентов после работы операции. Модели пост-ситуаций могут иметь модельное состояние и методы-синхронизаторы, так же, как и контрактные спецификации.
Модели ситуаций зависят от проверяемых компонентов, моделей поведения или теста, сообразно тому, в каких терминах они описывают ситуации.
Модели ситуаций могут быть сгенерированы автоматически из моделей поведения, интерфейсов и кода проверяемых компонентов, поскольку критерии полноты тестирования на основе структуры кода или функциональности часто используются при построении тестов. Такие компоненты, генерируемые из других, далее будем называть вторичными.
- Тесты.
Тесты, так же, как и модели ситуаций, могут создаваться разработчиками или генерироваться из моделей поведения и интерфейсов тестируемых компонентов. Каждый тест должен определять последовательность обращений к операциям тестируемого компонента (быть может, состоящую из единственного обращения) и значения параметров этих обращений, тестовые данные.
- Для решения первой задачи можно использовать два подхода.
- На практике более часто применяется связка из автоматной модели теста и генератора путей по графу переходов автомата. Эта техника положена в основу ModelJUnit и NModel.
В этом случае автоматная модель теста должна определять методы, играющие роль действий, охранных условий, а также возвращающие текущее состояние автомата. Модель теста может зависеть от проверяемого компонента или от его модели поведения.
- Для ряда случаев более эффективно применять монолитный генератор последовательностей, использующий информацию о тестируемом интерфейсе.
- Генерация тестовых данных, особенно данных сложной структуры, может использовать большое количество компонентов, играющих различные роли.
- Первичные генераторы, которые строят объекты некоторого типа. Такой генератор может быть устроен как итератор по некоторой коллекции.
- Фильтры, выполняющие отсев данных, не удовлетворяющих определенным ограничениям.
- Решатели ограничений, прямым образом строящие данные, удовлетворяющие некоторым ограничениям.
- Комбинаторы, строящие данные сложного типа из простых объектов.
- Преобразователи, генерирующие данные некоторого типа по более простому кодированному их представлению.
- Адаптеры.
Адаптеры устраняют возможные расхождения между интерфейсами моделей и моделируемых ими компонентов.
Адаптеры зависят от проверяемых компонентов. В тех случаях, когда они отвечают за синхронизацию модельного состояния, имеется зависимость и от соответствующей модели поведения.
Можно отметить, что во многих случаях небольшие расхождения между модельным и проверяемым интерфейсами не требует написания адаптера, поскольку могут быть устранены указанием библиотечной процедуры преобразования. Это относится к случаям, в которых различия сводятся к отсутствию ряда параметров, перестановке параметров местами или простым преобразованиям типов, например, чисел в строки и обратно. Во всех этих случаях адаптер строится не вручную, а автоматически, лишь по указанию соответствующего преобразования в конфигурационном файле тестовой системы.
- Заглушки (stubs, test doubles).
Заглушки подменяют во время теста компоненты, от которых зависят проверяемые. Они бывают двух видов.
- Управляющая заглушка (mock) передает в проверяемый компонент какие-то данные в виде возвращаемых ее методами результатов и служит дополнительным источником воздействий на тестируемый компонент.
- Наблюдающая заглушка (test spy) фиксирует вызовы ее операций и их аргументы для проверки корректности сделанных тестируемым компонентом обращений.
В принципе, одна заглушка может играть обе роли одновременно, но на практике такая потребность возникает крайне редко (это признак очень сложной организации теста, которую, возможно, имеет смысл пересмотреть).
Если заглушки используются, проверяемый компонент зависит от них. Тест или модель поведения зависят от наблюдающей заглушки, которую они используют. И наоборот, управляющая заглушка сама зависит от теста, поскольку именно тест должен определять результаты очередного вызова ее операций.
- Вспомогательные компоненты. К вспомогательным относятся компоненты, решающие многочисленные задачи организации работы верификационной системы, интеграции ее составляющих и сбора информации о происходящих событиях.
- Трассировщики различных видов событий. По сути, к каждому компоненту прикрепляется отдельный трассировщик событий, связанных с этим компонентом. Все трассировочные сообщения собираются одним или несколькими генераторами трасс.
- Планировщики тестов, включающих асинхронные воздействия, отвечающие за создание отдельных процессов и нитей внутри тестовой системы и распределение воздействия по ним.
- Диспетчеры и синхронизаторы отдельных операций в асинхронных и параллельных тестах.
- Конфигураторы, определяющие связи внутри некоторых групп компонентов.
Рисунок 1 демонстрирует одну из возможных конфигураций тестовой системы на основе предлагаемой архитектуры. Связи между компонентами, изображенные на рисунке, представляют собой зависимости, характерные для компонентов такого типа (хотя не все возможные зависимости изображены). Связи генератора трассы и конфигуратора не показаны, поскольку все или почти все компоненты связаны с ними.
Рисунок 1. Схема построения тестовой системы.
Конкретный набор компонентов и связи между ними описываются в конфигурационном файле в XML-формате, поступающем в начале работы на вход контейнеру внедрения зависимостей, который инициализирует все компоненты и связывает их нужным образом. Такой способ задания связей позволяет строить различные конфигурации тестовой системы, не меняя кода ее компонентов, и даже не имея к нему доступа. Вместе с тем, возможно определение жестких связей в самом коде, а также более гибкое связывание с помощью аннотаций и создание специальных компонентов-конфигураторов, которые содержат явную инициализацию компонентов и связей между ними на базовом языке программирования.
Реализация предложенного подхода
Для реализации предложенной архитектуры в качестве базового языка программирования был выбран язык Java. Он обладает многими языковыми возможностями, необходимыми для описания различных ролей классов и методов, а также связей между компонентами: средствами описания декларативной информации об элементах кода в виде аннотаций и поддержкой получения в динамике информации о структуре компонентов и сигнатурах их операций (интроспекция или рефлексия).
В качестве контейнера внедрения зависимостей была выбрана открытая библиотека Spring [45,46], поддерживающая достаточно большой набор функций системы такого типа.
Для описания моделей поведения была разработана небольшая библиотека, похожая, с одной стороны, на библиотеки проверки утверждений в средствах модульного тестирования (используются разнообразные методы assert()) и, с другой стороны, на Microsoft CodeContracts (для доступа к результату операции и пре-значениям выражений используются методы result() и oldValue()). В отличие от CodeContracts поддерживается создание контрактных спецификаций, использующих модельное состояние. В разработанной библиотеке отсутствуют имеющиеся в CodeContracts кванторные выражения и поддержка статического анализа ограничений.
Для описания моделей ситуаций также создана небольшая библиотека, обеспечивающая трассировку информации о покрытии указываемых ситуаций.
Тесты оформляются в стиле, аналогичном ModelJUnit и NModel, но с некоторыми расширениями, частично заимствованными из TestNG.
- Поддерживается расширенная иерархия элементов тестов: тестовые наборы, тесты, тестовые классы, тестовые методы. Тестовый набор состоит из тестов, один тест может включать в себя несколько тестовых классов.
- Тестовый класс описывает расширенный конечный автомат.
- Состоянием этого автомата считается результат работы всех методов, помеченных аннотацией State. Такое решение делает возможным добавление новых элементов в состояние без модификации ранее написанного кода. Состояние теста является списком состояний входящих в него классов.
- Тестовые методы определяют действия автомата, возможно параметризованные. Значения параметров извлекаются из связанного с тестовым методом провайдера данных. Провайдер может быть генератором наборов значений, определенных, например, как элементы некоторой коллекции, а может быть построен динамически из генераторов данных для разных параметров с определенной стратегией их комбинирования (выбирать все комбинации, каждое значение каждого параметра, все возможные пары значений и пр.). Провайдеры данных и способ их комбинирования задаются с помощью аннотаций метода и его отдельных параметров.
- Действия могут иметь охранные условия, оформляемые в виде методов, возвращающих булевский результат и зависящих от состояния объекта тестового класса. Охранное условие привязывается к тестовому методу при помощи аннотаций, без использования соглашений об именовании методов. Поэтому одно и то же условие может быть использовано для разных методов, и один метод может иметь несколько охранных условий. Кроме того, охранные условия могут иметь в качестве параметров любой набор, являющийся началом набора параметров соответствующего метода, в том числе пустой (в этом случае охранное условие зависит только от текущего состояния).
- Так же, как в TestNG, любой тестовый элемент — набор, тест, класс, метод — может иметь конфигурационные методы инициализации и финализации. Дополнительно можно определять конфигурационные методы, вызываемые при посещении очередного состояния.
Для построения заглушек используется свободная библиотека Mockito [26]. Она имеет достаточно богатые возможности для определения управляющих и наблюдающих заглушек и использует интуитивно понятный синтаксис при их описании. Этот пример показывает, что при наличии Java-библиотеки с необходимой функциональностью, она без особых усилий может быть использована в рамках предлагаемой архитектуры.
Назад Содержание Вперёд
|
|