Архитектура автоматизированных функциональных тестов: прагматичный подход к использованию Model-Based техник автоматизированного тестирования |
29.09.2008 10:50 | ||||
Автор: Михаил Давыдов, Luxoft Company (www.luxoft.com) Суть тестирования — выполнение проверок (в model-based тестировании их принято называть «оракулами»). Очевидно, что при проведении проверок можно и нужно абстрагироваться от пользовательского интерфейса. Проверки выполняются над объектами, представляющими состояние системы (причём, могут использоваться несколько объектов одновременно — например, объекты представляющие текущее и предыдущее состояния системы). Функциональное автоматизированное тестирование — постановка задачиЦель функционального тестирования — проверить, что тестируемая система удовлетворяет требованиям документа SRS (функциональной спецификации). Наиболее распространённые техники автоматизированного тестирования — модульное (компонентное, Unit) тестирование и «регрессионное» тестирование (основанное на идее Baseline — сохранении выходных данных тестируемой системы для последующего сравнения) не удовлетворяют этой цели. Однако существующие на рынке инструменты заточены, как правило, под один из этих двух видов автоматизированного тестирования. Open-source инструменты, как правило, больше подходят для модульного тестирования, а коммерческие инструменты таких компаний как Mercury Interactive, IBM/Rational, Borland/Segue, Empirix и др., скорее подходят для регрессионного тестирования (и значительная часть встроенной в них функциональности рассчитана именно на такой подход). В строгом смысле слова, единственной техникой функционального тестирования является тестирование на основе формальных спецификаций (Model-based testing). Однако этот подход обычно применяют для промышленных систем, к качеству которых предъявляются повышенные требования. Такие системы, как правило, не имеют развитого графического пользовательского интерфейса. С другой стороны, Model-based тестирование — весьма дорогое удовольствие, требующее как исключительной квалификации сотрудников, так и применения дорогостоящего (даже по сравнению с коммерческими инструментами автоматизации тестирования) и сложного в освоении программного обеспечения. Итак, возникает вопрос — можно ли использовать привычные инструменты (как коммерческие, так и open-source) для проведения автоматизированного функционального тестирования систем с развитым графическим пользовательским интерфейсом? Может ли такое тестирование быть экономически оправданным (по сравнению с ручным тестированием)? Мой ответ — да. Единственное что для этого надо — понимать цели функционального тестирования, и не увлекаться формой в ущерб содержанию — то есть, использовать «прагматичный» подход. Описываемый здесь подход не уникален (см., например, статью Гарри Робертсона), но в моей статье я постараюсь сосредоточиться не на том, зачем нужно Model-based тестирование, а на архитектуре Model-based тестов, применимой в реальной жизни. Разбираем автоматизированный функциональный тест на составные частиЧтобы разработать оптимальную архитектуру автоматизированных функциональных тестов, надо понять, из каких «частей» состоит типичный функциональный тест. Итак: Драйвер интерфейсаФункциональное (как и регрессионное) тестирование — это тестирование методом «черного ящика», то есть, тестирование через непосредственное взаимодействие с пользовательским интерфейсом тестируемой системы. Что для этого нужно — это набор функций, осуществляющих непосредственное взаимодействие с системой, позволяющий, тем не менее, абстрагироваться от конкретной реализации пользовательского интерфейса. Причём, нам придется, как вводить данные в тестируемую систему, так и «захватывать» выходные данные. За это отвечает модуль, называемый «драйвером интерфейса». В качестве драйвера интерфейса можно использовать непосредственно встроенные функции инструмента автоматизации. Препятствием тут может служить их недостаточный уровень абстракции. Ещё одна проблема с использованием встроенных функций — интерфейс функций тестового драйвера должен облегчать интеграцию с другими модулями функционального теста, о которых речь пойдёт дальше. Хранение внутреннего «состояния» системыНа более высоком, чем пользовательский интерфейс системы уровне абстракции существует такое понятие, как состояние тестируемой системы. Это состояние полностью определяется входными (тестовыми данными). Синоним понятия «состояние системы» — выходные данные системы. Причём, в каждый момент времени существует целый набор параметров системы («переменных состояния») из которых складывается состояние системы. Так, предположим, что мы тестируем текстовый редактор. Состояние главного окна текстового редактора описывается большим набором переменных состояния. Наиболее очевидные из них:
Между переменными состояния существуют сложные взаимосвязи. Так, наличие или отсутствие открытого документа определяет доступность команд «Сохранить» и «Сохранить как...» (это тоже — переменные состояния) Ещё одна важная особенность состояний тестируемой системы — то, что переменные состояния отдельных частей (компонентов) системы могут не зависеть друг от друга. Точнее говоря, состояния разных компонентов системы имеют ограниченное количество общих переменных состояния. Так, диалог открытия файла того же тестового редактора «знает» только имя и папку последнего открытого файла, но ничего не знает ни о положении курсора, ни о наличии выделения текста в главном окне. Для того чтобы предсказать, к какому результату «должны» (согласно функциональной спецификации) привести действия над тестируемой системой необходимо и достаточно знать набор переменных состояния каждой части (компонента) системы и взаимосвязи между ними, а также взаимосвязи между компонентами системы. На практике это означает, что для каждого компонента («страницы» в случае веб- приложения, либо «окна» в случае Win32-GUI приложения) системы удобно иметь объект (структуру), хранящий все переменные состояния данного компонента. Причём, данные в этом объекте должны храниться в таком виде, который облегчает сравнение состояний разных компонентов, сравнение разных состояний одного и того же компонента, облегчает «сериализацию» (преобразование в другие форматы, в том числе, сохранение в файл и вывод в отчёт о выполнении теста). Итак — ещё один модуль функционального теста — набор классов, представляющих состояние компонентов тестируемой системы, и соответствующие им объекты (далее — объекты состояния компонента системы). Взаимосвязи между состояниями системыЗдесь придётся сделать небольшое отступление. Дело в том, что взаимосвязи между переменными состояния системы (фактически представляющие формализованную функциональную спецификацию системы) в нашем подходе не выделяются (в отличие от остальных «составляющих» функционального теста) в отдельный компонент. Напротив, при прагматичном подходе к model-based тестированию, эти знания о системе «размазываются» по нескольким компонентам — оракулам, «менеджеру функционального тестирования», генератору тестовых данных (при его наличии), и т.д. Функциональную спецификацию можно представить в виде набора переменных состояния каждого компонента системы (их, как уже договорились, мы будем хранить в специальных структурах/объектах) с одной стороны, и набора взаимосвязей — как внутренних (между переменными состояния одного компонента) так и внешних — между переменными состояния разными компонентами с другой. Эти взаимосвязи, в идеале, также нужно формализовать и хранить в определённом виде. Очевидно, способ хранения этих взаимосвязей должен учитывать способ хранения самих переменных состояния, и способ использования этих взаимосвязей в тесте. Причём важно понимать, что эти взаимосвязи — это, по существу - функции. Это могут быть настолько простые функции, что их можно хранить просто в «табличном» виде, а могут быть и сложные математические функции. Это тоже накладывает ограничения на способ хранения. Взаимосвязи между переменными состояния нам понадобятся для двух различных целей — для определения доступных действий над компонентами (это может понадобиться для автоматической генерации тест кейсов) и для функциональных проверок.
На практике это означает, что наиболее часто используемые «константы» хранятся в отдельных файлах (не обязательно файлах с данными — это могут быть и программные модули с определениями констант), а более «частные» (и более сложно формализуемые) можно «жестко кодировать» в месте использования. Формат же хранения этих данных должен обеспечивать максимальное удобство, как при редактировании констант, так и при их использовании. Тестовые оракулыСуть тестирования — выполнение проверок (в model-based тестировании их принято называть «оракулами»). Очевидно, что при проведении проверок можно и нужно абстрагироваться от пользовательского интерфейса. Проверки выполняются над объектами, представляющими состояние системы (причём, могут использоваться несколько объектов одновременно — например, объекты представляющие текущее и предыдущее состояния системы). Оракулы же хранят и данные о взаимосвязях переменных состояния и частей системы в целом. Поэтому логично выделение оракулов в отдельный компонент (модуль, библиотеку, либо набор классов). В этом же модуле, зачастую, хранятся и взаимосвязи между переменными состояния — как в виде общих констант, так и жестко закодированные в «теле» проверок. Возникает вопрос — что должно входить в отдельные функции-оракулы? По моему опыту, удобнее всего создавать отдельные функции-оракулы на каждое действие над тестируемой системой, приводящее к изменению её состояния.
Менеджер (диспетчер) функционального тестированияРасполагая знаниями о взаимосвязях между компонентами системы и переменными состояния внутри каждого компонента, мы можем с помощью оракулов предсказать, в какое состояние переведёт систему то или иное действие. Однако, оракулы сами по себе ничего не «знают» о том, как выполнять действия над системой (это «знает» драйвер интерфейса), а драйвер интерфейса «не в курсе», какие проверки необходимо вызывать при выполнении того или иного действия. Кроме того, для работы оракулов нужны значения переменных состояния (причём, как правило, используются сразу несколько «снимков» состояния системы — например, «снимок» текущего и предыдущего состояний). «Захват» и хранение состояний компонентов системы также должны координироваться отдельно. Таким образом, нам понадобиться ещё один модуль функционально теста «менеджер (диспетчер) функционального тестирования». Как у всякого менеджера, его основная задача — координировать работу подчинённых — в данном случае, драйвера пользовательского интерфейса и оракулов, и предоставлять им необходимые в их работе данные. Тестовые данные (тест-кейс)Для выполнения теста нам не хватает «самой малости» — последовательности действий (набора тестовых данных). Существует несколько подходов к созданию и хранению тестовых данных. Можно тестовые данные задавать вручную. В этом случае проще всего в «главном» модуле тестов непосредственно вызывать методы (это может быть и единственный метод) менеджера функционального тестирования, которые и будут инициировать как непосредственное взаимодействие с тестируемой системой, так и проверку результатов этих действий. Очевидно, что «параметры» этих вызовов, при желании можно хранить в отдельном файле, который и будет соответствовать понятию «тест кейса» в ручном тестировании. Если система сложная, для обеспечения полноты набора тестов нам могут понадобиться десятки, сотни, и тысячи тест кейсов. В этом случае нам будет нужен ещё один модуль. Автоматизированная генерация тест кейсовОчевидно, что длина последовательности действий над системой может быть бесконечной, как и набор всех возможных комбинаций входных данных. При создании тест-кейса (неважно, вручную либо автоматически) необходимо ограничить как длину последовательности действий, так и набор тестовых данных. Если говорить об автоматизированной генерации тест-кейсов, то тут (в рамках «прагматичного» подхода) имеет смысл использовать один из двух различных методов ограничения набора тестовых данных: 1. Ограничиться генерацией входных данных для отдельных компонентов системы и их непосредственного взаимодействия с соседями. То есть, для каждого компонента, набор действий и тестовых данных генерируется отдельно. Выбор комбинаций тестовых данных можно поручить специализированным компонентам (например, программе ALLPAIRS). Это — «классический» подход к автоматизированной генерации данных. Его недостаток — необходимости в отдельном генераторе данных для каждого компонента (экрана, окна, страницы) системы. 2. Создать «модель состояний» системы, описывающую доступные действия над системой в каждом её состоянии. Набор данных для каждого действия можно ограничить по «доменному» принципу (на каждую ветку выполнения по одному набору значений). Затем случайным образом сгенерировать последовательности действий с соответствующими входными данными. Для этого придется ввести систему «приоритетов» различных действий, систему ограничений длинны последовательности действий, и т.п. Такой способ можно назвать «Smart Monkey Testing», он упоминается Кемом Канером в его презентации «High Volume Automated Testing». Недостаток такого подхода — то, что при всей широте тестового покрытия, его «глубина» при тестировании достаточно сложной системы и разумном (до нескольких дней) времени выполнении такого теста весьма небольшая. И, понятно, этот метод не даёт никаких гарантий (хотя и позволяет находить дефекты, которые никогда бы не были найдены при использовании других подходов). Ещё одна проблема — отладка такого теста. Архитектура функциональных тестовИтак, попытаемся теперь изобразить получившуюся архитектуру функционального теста. Для простоты опустим несколько связей (так, все библиотеки могут записывать сообщения в лог, но на рисунке этой «привилегией» обладает лишь менеджер функционального тестирования). Как видно, функциональный тест состоит из нескольких «программных» модулей (обозначены прямоугольниками со сглаженными углами), и нескольких «модулей данных» (на практике, модули данных также могут быть программными, то есть содержать код — например, объявление констант), которые обозначены в виде листа бумаги с загнутым уголком: Тест-кейс содержит последовательность действий над тестируемой системой и тестовые данные для каждого действия (может состоять из нескольких файлов — например, тестовые данные можно выделить в отдельный файл, а «драйвер данных» этого файла (который и будет вызвать методы менеджера функционального тестирования) — в другой). Менеджер функционального тестирования вызывается из тест-кейса для выполнения действий над тестируемой системой и выполнения проверок. Он управляет сохранением состояний системы, вызовом функций «интерфейсного драйвера» и соответствующих им оракулов. Менеджер функционального тестирования удобнее всего выделить в отдельный модуль. Библиотека оракулов. Содержит проверки функциональности. Использует константы, описывающие поведение системы (могут «физически» в том же файле, что и сами оракулы). Использует объекты состояния системы для получения переменных состояния каждого компонента системы. Объекты состояния системы для оракулов предоставляет «менеджер функционального тестирования». Библиотека классов состояния системы. Содержит классы, хранящие переменные состояния (выходные данные) для каждого компонента (экрана, окна, страницы). Интерфейсный драйвер содержит функции для непосредственного взаимодействия с системой через её пользовательский интерфейс. Обычно интерфейсный драйвер выделяется в отдельный модуль, однако в простых случаях в качестве интерфейсного драйвера можно использовать встроенные функции инструмента автоматизации тестирования. Константы, описывающие поведение системы — взаимосвязи между переменными состояния (а также взаимосвязи между частями системы) могут быть выделены в отдельный файл «данных», а могут быть составной частью библиотеки оракулов Генератор тестов — опциональный компонент, который может использоваться для генерации тестовых данных и последовательности действий над тестируемой системой. В зависимости от используемого подхода, генератор тестов может либо генерировать данные, используемые затем в качестве тест-кейса, а может и в реальном времени вызывать менеджер функционального тестирования. Отчёт о выполнении теста содержит сообщения всех программных модулей — менеджера функционального тестирования (о выполненных действиях над системой), библиотеки оракулов (о проваленных проверках, а также о результатах проверок, требующих дополнительного «ручного» анализа), интерфейсного драйвера (о проблемах взаимодействия с пользовательским интерфейсом системы). Рис.1. Архитектура типичного автоматизированного функционального теста
Разработка функциональных тестов с использованием коммерческих инструментов автоматизации тестирования, или «дьявол в деталях»Пока что, мы почти не говорили о том, как использовать возможности существующих инструментов автоматизации тестирования для реализации нашей архитектуры. Дьявол, как известно, кроется в деталях, и с того момента, как я задумался о применении Model-Based подхода, до того момента, как стало ясно, как это можно сделать, используя стандартные инструменты (в моём случае — Mercury Quick Test Pro), прошло больше года (впрочем, это время не прошло впустую). Увы, те статьи, которые я видел по model-based тестированию, прекрасно расписывают все его «вкусности», но мало говорят о том, как его применять на практике. Об этом и поговорим. Общие библиотеки функцийПервое, что бросается в глаза — Model-Based тестирование основано на манипуляции данными тестируемой системы — данными, имеющими сложную структуру. Фактически, нам для проверки правильности работы алгоритмов тестируемой системы придётся писать собственные, пусть, упрощенные, версии тех же алгоритмов (метод функциональной эквивалентности). Увы, ни один существующий коммерческий инструмент (кроме, разве что, Rational Functional Tester'a, который унаследовал это прекрасное свойство от своих языков тестов — VB.Net и Java) не имеет развитых средств хранения и манипулирования данными. Это означает только одно — нам придется разрабатывать эти средства самостоятельно. Наиболее распространенные типы данных, с которыми нам придется работать — табличные данные, двумерные и одномерные массивы, а также «ассоциативные массивы». Кроме структур, умеющих хранить эти типы данных (они, зачастую, уже есть в скриптовом языке) понадобятся функции для манипуляции данными. Надо научиться сравнивать данные, искать их и переносить по частям из одного «объекта» в другой. В случае QTP, мне пришлось написать библиотеки для работы с массивами, создать класс для работы с табличными данными, библиотеку для работы с объектами типа "Dictionary" (ассоциативными массивами) и много других функций для манипуляции данными. Для того чтобы работать с данными системы, их сначала надо получить, причём в нужном нам виде — виде «объектов», с которым мы и будем работать. Для этого понадобятся дополнительные функции «съема» данных. Главное, что их будет отличать от встроенных функций инструмента автоматизации — данные нам надо будет снимать «оптом», а не «в розницу». Вводить данные в систему также удобнее всего «оптом» — тут тоже понадобятся специальные функции (заточенные под конкретную тестируемую систему). Например, если речь идёт о вёб-приложении, нам понадобятся функции для получения значений элементов «вёб-форм» (то есть, всех элементов INPUT), и функции для заполнения тех же форм. Содержимое формы удобно представить в виде ассоциативного массива «имя элемента — значение». По существу, полученный массив будет готовым поднабором переменных состояния страницы. Заполнять форму удобно в том же формате — передавать строку вида "элемент1=значение1;элемент2=значение2;..." (это — самый удобный способ задать ассоциативный массив). Единственным «вещественным» результатом выполнения теста является отчёт о выполнении. Поэтому очень важно уметь правильно работать с отчётом — например, разбивать отчёт на отдельные «шаги», отслеживая результат каждого шага, уметь записывать в отчёт наборы данных в удобном формате (например, в виде таблиц HTML). Для этого тоже понадобится писать свои функции. Константы, описывающие поведение системы, тестовые данные надо где-то хранить. Хранить в удобном формате, чтобы их можно было легко редактировать, а ещё легче — использовать в тесте, избегая путаницы. Для этого понадобится ещё один набор функций. К хранению данных может быть много разных подходов. Можно хранить данные в таблицах Excel либо comma-delimited (CSV) файлах, можно — в формате XML, можно — в самом тесте в виде строк (в уже упоминавшемся формате "элемент=значение;..." — мне этот подход оказался ближе остальных). Увы, и тут встроенной функциональностью инструмента обойтись не удастся (с ужасом вспоминаю результаты своих попыток использовать для этих целей DataTable QuickT est P ro ). Классы состояния системыОчень удобно, если «компоненты» системы имеют много общих элементов интерфейса (и, соответственно, общих переменных состояния). В этом случае для хранения состояний разных «экранов» можно использовать одни и те же структуры данных. В общем же случае, на каждый «экран» надо иметь свою, отдельную структуру данных, с другой стороны, некоторые элементы этой структуры, тем не менее, могут быть и общими. Очевидно, что реализация классов очень сильно зависит от скриптового языка. Прекрасно, если язык объектно-ориентированный. В этом случае можно будет наследовать классы состояния друг от друга. Мне достался несколько менее удачный случай — скриптовый язык QTP — VBScript является объектным, но не объектно-ориентированным. Соответственно, классы друг от друга наследовать нельзя. Но зато, классы могут иметь свои методы, и, как и любой язык с поздним связыванием, VBScript не проводит проверку соответствия типов. Поэтому, классы могут иметь «одноименные» методы — весьма удобно (хоть и не «красивый стиль» — придется активно применять «национальный китайский способ повторного использования кода» — Copy-Paste ). Ещё несколько худший вариант — язык, поддерживающий структуры, но не поддерживающий классы. Тут придется ограничиться хранением данных в структурах. Тоже неплохо. Ещё хуже — если язык поддерживает только скалярные типы данных и массивы (WinRunner/TCL, Rational Robot/SQABasic и прочие инструменты первого поколения, кроме, разве что SilkTest 'a, сильно обогнавшего время). Единственный вариант, который я вижу в этом случае — писать объекты на «полноценном» ООП-языке,например, C++ или Delphi, и работать с ними через DLL . Остальные варианты напоминают, нет, не «забивание гвоздей микроскопом», а скорее попытки подковать блоху паровым молотом с ЧПУ ;). Собственно, идеи о внедрении Model-Based практик впервые пришли мне в голову ещё в период моей работы с WinRunner, но тогда всё упёрлось именно в отсутствие способов реализации объектов состояния на TCL — все решения, которые я видел, были слишком громоздкими. Итак, вернёмся к примеру с текстовым редактором, и попробуем представить, как будет выглядеть (в упрощенном виде) класс состояния главного окна.
Отдельно понадобятся классы состояния для главного меню, для контекстного меню, для диалогов (например, диалог выбора формата). Будут нужны и функции, «схватывающие состояния» каждого из компонентов — в зависимости от скриптового языка, они могут быть либо методами объекта состояния, либо отдельными функциями. ОракулыНаиболее часто используемый тип проверок в функциях-оракулах — сравнение данных. Приведу несколько примеров проверок для того же текстового редактора:
Я не буду здесь приводить примеры констант, описывающих поведение системы в контексте выбранного примера, так как использование таких констант здесь выглядит несколько натянутым. Приведу более близкий моей практике пример — проверка правильности сортировки таблицы (после выполнения сортировки по одной из колонок). Для такой проверки нам надо будет знать формат каждой колонки (число/строка/дата). Если таблиц много, то здесь удобно применить константы следующего вида (пример на VBScript):
Потом эти же константы нам могут пригодиться и для других проверок. Например, можно будет проверить формат данных в колонках таблицы, используя эти же константы. Другой пример — взаимосвязь переменных состояния на одном экране. Пусть это форма поиска данных с таблицей результатов. Тогда у нас будут такие переменные состояния:
Для проверки зависимостей между переменными состояния нам понадобится константа SearchForm_dependancies:
Чтобы проверить значения фильтров по умолчанию, используем другую константу:
Соответствующие функции-оракулы, использующие данные константы довольно просты, но требуют использования некоторых функций, отсутствующих в VBScript — для сравнения объектов Dictionary, прежде всего. Очевидно, все те же задачи можно было решить, используя XML либо файлы CSV. Только вот таблицы CSV — двумерны (а некоторые коллекции из примера — трёхмерны), а использовать XML в данном случае неоправданно. Менеджер функционального тестированияРассмотрим самый простой (и универсальный) вариант организации менеджера функционального тестирования, применимый для нашего примера — теста текстового редактора. Хранение и «запоминание» состояния компонентовДля каждого компонента у нас будет храниться по два состояния — текущее (оно же — последнее схваченное) и предыдущее (схваченное перед последним действием надо компонентом, изменившим его состояние). Для каждого компонента у нас будет определено по функции, которая и будет последнее схваченное состояние переносить в переменную прошлого состояния, и захватывать новое состояние компонента. Пример: Функции «действий»Для каждого действия над тестируемой системой создается «функция-обертка», выполняющая несколько действий:
Обработка исключительных ситуацийВ самом простом случае, в исключительной ситуации выставляется флаг ошибки, в результате чего последующие вызовы функций-обёрток не проходят до тех пор, пока не будет вызвана функция, инициализирующая состояние системы заново (в нашем примере — перезапуск редактора) Тест-кейсВ простейшем случае, тест-кейс состоит из вызовов «функций-оберток» менеджера функционального тестирования. Можно организовать и обработку исключительных ситуаций. Посмотрим, как будет выглядеть простейший тест-кейс: Тест-кейс ( VBScript ):
А теперь проследим за вызовами функций (в квадратных скобках указано имя модуля) (проверка исключительных ситуаций опущена, встроенные функции и функции общих библиотек опущены):
ЗаключениеАвтор не претендует на точность использования терминологии, а предлагаемый подход — на академичность. Главное достоинство предложенного подхода — он работает, позволяя создавать сложные, достаточно легко поддерживаемые тесты в реалистичные сроки, используя привычные инструменты. |