Введение
Слышали ли вы когда-нибудь о Continuous Integration? Скорее всего, да. Continuous Integration, или непрерывная интеграция восходит к статье Мартина Фаулера, в которой изложены базовые подходы к эффективной разработке программного обеспечения. Что скрывается за термином "Сontinuous Integration" и каким образом непрерывная интеграция улучшает качество выпускаемых программных продуктов?
Рассмотрим стадии конвейера "разработка|тестирование". На входе есть некоторый набор исходных файлов и вспомогательных ресурсов. Из этого набора можно собрать конечный продукт. Но пока он не собран, можно проверить работоспособность отдельных компонентов кода. Эту задачу решают unit-тесты - небольшие блоки, тестирующие "основной" программный код. Если тесты отрабатывают успешно, то мы сможем проверить работоспособность более сложных компонентов, а также их взаимодействие. На данном этапе можно собрать и проверить функциональность отдельных частей продукта. Если и здесь всё хорошо, то мы собираем весь продукт и проводим его полное тестирование. На каждой стадии могут быть выявлены ошибки. Более того, исходный код того или иного компонента может просто не собраться. В идеале, переход на следующую стадию осуществляется при условии отсутствия ошибок на более ранних стадиях. Таким образом, на выходе конвейера получается либо собранный продукт, либо очередная порция дефектов для исправления.
Конвейер - достаточно точное определение. Описанный выше процесс повторяется циклически для каждого обновления исходного кода. Одни и те же рутинные операции, одни и те же проверки. Чтобы избавить инженера от рутины и делегировать ему более интеллектуальные задачи, часть этапов можно автоматизировать. Во-первых, компиляция и сборка зачастую представляют собой выполнение конечной последовательности команд с определенными параметрами. Запуск unit-тестов изначально предполагался в автоматическом режиме. Можно автоматизировать и выполнение API-тестов. В конечном итоге, собранный продукт можно будет рассматривать как "черный ящик"(black box), готовый к тестированию на всех уровнях (GUI, Web-cервисы, протоколы, взаимодействие с базами данных и тому подобное). Для автоматизации тестирования каждого уровня есть свои средства, представленные либо в виде библиотек, либо в виде отдельных программных систем. Иными словами, выполнение каждого этапа тестирования можно автоматизировать. А что если автоматизировать всю цепочку? В результате получаем некий пакетный файл, в котором заложено последовательное выполнение инструкций по сборке, установке продукта и запуску тестов. На выходе будет набор результатов выполнения тестов, по которым можно корректировать дальнейшие действия (кстати, и этот процесс можно частично автоматизировать).
Обычно непрерывная интеграция реализуется на отдельном сервере с cоответствующим программным обеспечением для сборки и запуска тестов. Необходима система контроля версий, в которой хранятся файлы с исходным кодом тестируемого приложения, а также непосредственно сами тесты. В заданное время или по команде извне из системы контроля версий извлекаются файлы с исходным кодом и служебные компоненты, которые размещаются в соответствующих каталогах. После этого выполняется сборка и тестирование продуктов с выдачей отчетов по результатам каждого этапа.
Преимущества непрерывной интеграции:
- При регулярных запусках конвейера период между появлением ошибки и ее обнаружением минимален или, по крайней мере, стабилизирован.
- Выбор последовательности этапов по принципу увеличения абстрагированности тестов от исходного кода позволяет выявить ошибку именно на том уровне, на котором она допущена.
- Вмешательство инженера минимально: запустить всю систему, собрать и обработать результаты.
- Процесс тестирования интегрирован в разработку.
Возможность организовать непрерывную интеграцию есть во многих системах. Например, Visual Studio Team System содержит инфраструктуру, охватывающую все стадии разработки приложения. Cреди продуктов, позволяющих работать с пакетными файлами, можно отметить ant-решения. Система CruiseControl может управлять запусками на разных машинах. Таким образом, средства для внедрения непрерывной интеграции уже есть. Важно лишь определиться с подходами к выполнению каждого этапа и всего процесса в целом.
Постановка задачи
Всё вышеперечисленное - не более чем теория. Когда нет практической составляющей, теория мало о чем скажет. Поэтому рассмотрим конкретный пример построения технологической цепочки. В данной статье мы рассмотрим только одну из частей данного конвейера: "система контроля версий - средство тестирования - багтрекер", так как данная статья больше предназначена для тех, кто работает в тестировании и основная цель данного материала - применить практики и средства, используемые разработчиками в целях тестирования. Этим самым мы как бы сближаем разработку и тестирование, делая эти процессы жестко завязанными друг на друга. Кроме того, рассматриваемая часть конвейера характерна тем, что именно на данном участке используемые инструментальные средства зачастую хуже всего взаимосвязаны и меньше всего интегрированы. Cказывается еще и тот факт, что вышеприведенное сочетание часто формируется на разных этапах и для разных целей. Так, например, система контроля версий выбирается еще на начальных этапах разработки и если обеспечивается ее интеграция с чем-то, то скорее всего это средство разработки. Баг-трекер тоже должен быть выбран сразу. Как минимум, он нужен, когда уже появляются непосредственно сами баги. Зачастую он не интегрирован с остальными системами. В свою очередь, средство автоматизированного тестирования (особенно, если тестирование высокоуровневое) может быть выбрано, когда весь объем тестирования становится непросто охватить усилиями тестировщиков. Данный случай достаточно тяжелый, но в то же время является распространенным явлением.
Рассмотрим пример формирования связки следующих систем:
- Система контроля версий - Subversion
- Средство автоматизированного тестирования - AutomatedQA TestComplete
- Багтрекинговая система - Mantis
Перечисленные системы автоматически связываются с помощью ANT. Нам нужно построить решение, которое в автоматическом режиме выполнит следующее:
- Извлечет файлы с тестами в заданное расположение
- Запустит выполнение тестов
- Отправит выявленные ошибки в багтрекер с указанием дополнительной информации, позволяющей описать суть выявленных проблем
На выходе получим ANT-скрипт, содержащий все необходимые задачи и выполняемый путем запуска из командной строки. Изначально предположим, что ANT, TestComplete и Mantis уже установлены, а все необходимые атрибуты известны.
Подготовка среды
Для начала нам желательно в переменной окружения PATH прописать пути к ANT, SVN и TestComplete, чтобы в дальнейшем вызывать эти приложения из любого каталога не используя абсолютные пути.
Перейдем к каркасу ANT-скрипта, который постепенно будет наполняться задачами для выполнения. Создадим файл build.xml и наполним его содержимым. Выглядит это так:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="build" name="SampleIntegration">
</project>
Когда надо извлекать какие-либо компоненты и производить с ними различные манипуляции, нужно прежде всего определиться, куда все это извлекать. Иными словами, нужен рабочий каталог. Если ANT и TestComplete установлены на машине, на которой будет организован прогон цепочки, то Mantis - это отдельная система, которая может находиться на удаленной машине. При этом мы должны знать путь, по которому обращаться с багтрекеру. Таким образом, мы приходим к выводу, что нам нужно будет где-то задавать параметры конфигурации.
Для этих целей в ANT-скрипт подключаются файлы свойств. Это обычные текстовые файлы, в которых свойства перечислены отдельными строками вида <Имя свойства> = <Значение>. В нашем случае нужно задать 2 параметра:
- Рабочий каталог
- Адрес SVN-репозитория, из которого будут извлекаться файлы
- Логин и пароль для SVN
- Адрес Mantis
- Имя проекта Mantis
- Имя компонента в Mantis, для которого создаются записи
- Логин и пароль пользователя Mantis
- Версия и билд тестируемого продукта
- Различные атрибуты, отражающие статусы создаваемой записи в Mantis (view state, status, resolution, severity)
- Операционная система
Cоздадим файл config.properties, поместим его в каталог с build.xml и запишем строки вида:
working_dir=c:/test_dir
svn_repo=svn://localhost:3909
svn_user=test
svn_pass=password
mantis_url=http://localhost:8008
mantis_prj=Test
mantis_component=Sample
mantis_user=administrator
mantis_pass=root
mantis_prjver=1.0
mantis_build=1.0.0
mantis_view_state=public
mantis_status=new
mantis_resolution=open
mantis_severity=minor
mantis_os=WinXP
После этого в файле build.xml подключим файл свойств. Теперь ANT-скрипт имеет вид:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="build" name="SampleIntegration">
<property file="config.properties"/>
</project>
Далее нам нужно подготовить каталог, в который будут извлечены тесты и удостовериться, что каталог пустой. Соответственно, если каталог существует, то нам надо его удалить. Затем можно будет создать новый. Подготовим последовательность шагов, которая это реализует. Текст ANT-скрипта:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="build" name="SampleIntegration">
<property file="config.properties"/>
<target name="testCleanup">
<delete dir="${working_dir}" />
<mkdir dir="${working_dir}" />
</target>
</project>
Итак, мы обеспечили чистую среду для работы с файлами.
Извлечение файлов
В созданный рабочий каталог нужно перенести файлы, которые хранятся в репозитории SVN. Для работы с SVN через ANT есть готове решение. Из всего набора файлов нам нужны:
- svnant.jar
- svnClientAdapter.jar
- svnjavahl.jar
Эти файлы надо поместить рядом с файлами build.xml и build.properties. В рабочем каталоге создадим папку lib и переместим в нее вышеуказанные jar-файлы.
Далее, надо указать пути к упомянутым jar-файлам. Для этого сразу под строкой подключения файла свойств добавим следующие строки:
<path id="project.classpath">
<pathelement location="./lib/svnjavahl.jar" />
<pathelement location="./lib/svnant.jar" />
<pathelement location="./lib/svnClientAdapter.jar" />
</path>
Вот теперь мы можем добавить новый target, который извлечет файлы из SVN-репозитория в подготовленный каталог. Реализуется это так:
<target name="testGetFiles" depends="testCleanup">
<taskdef name="svn" classpathref="project.classpath"
classname="org.tigris.subversion.svnant.SvnTask" />
<svn username="${svn_user}" password="${svn_pass}">
<checkout url="${svn_repo}" destPath="${working_dir}" />
</svn>
</target>
Данный фрагмент выполняет checkout файлов из репозитория в заданный каталог. Обратите внимание, что данный target зависит от задачи testCleanup. То есть, перед вызовом задачи testGetFiles автоматически выполнится задача testCleanup.
Написание скриптов на TestComplete
Создание проекта
Итак, мы создадим проект, в котором будут находиться тесты и вспомогательные модули, отвечающие за импорт результатов в багтрекер. На начальном этапе у нас пустая рабочая область. Создание всех необходимых компонентов происходит в такой последовательности:
- Выберем меню File > New > New Project Suite
- Зададим имя нового Project Suite. Для нашего тестового примера он будет называться Sample. Также укажем каталог, в котором будет храниться наш проект, и нажмем ОК.
- В полученном Project Suite создадим новый проект. На панели Project Workspace выбираем созданный Project Suite, кликаем на нем правой кнопкой мыши и в выпадающем меню выбираем Add > New Item.
- Появится диалог, в котором мы зададим имя проекта (в нашем случае это будет SampleWebService), выберем язык написания тестов (в примере используется JScript) и нажмем ОК.
- Поочередно добавим в проект следующие компоненты: Script, Events, WebServices. Все эти компоненты добавляются путем клика правой кнопкой мыши на проекте и выборе пункта меню Add > New Item, после чего в списке надо выбрать соответствующий элемент.
- Теперь добавим новый объект для работы с веб-сервисами и настроим его для работы с Mantis. Порядок создания нового компонента в TestComplete для работы с веб-сервисами детально описан здесь, поэтому останавливаться на нем не будем. Единственное, что нужно отметить: расположение WSDL-описаний для Mantis представлено по адресу http://<mantis_host>/api/soap/mantisconnect.php?wsdl, где mantis_host - хост, на котором находится целевой багтрекер.
- В элемент Script добавим cледующие файлы:
- _Events - сюда мы поместим обработчики событий TestComplete
- _Logger - здесь будут находиться вспомогательные функции, используемые для логирования
- _MantisConfig - здесь будут храниться переменные, отвечающие за основные атрибуты, необходимые при работе с Mantis
- _MantisUtils - вспомогательные функции, предназначенные для работы с Mantis
- SampleTest - будет содержать небольшой тест, который просто выведет несколько сообщений. Одно из таких сообщений - это сообщение об ошибке
- Кликаем на элементе Events > General Events для появления формы настройки событий.
- Кликаем на событие OnLogError и на кнопку New, которая появится сразу после выделения нужного события.
- В появившемся диалоге выбираем файл _Events для хранения обработчика данного события и нажимаем ОК - код обработчика сгенерируется автоматически
Итак, основная структура проекта у нас готова. Перейдем к реализации.
Логирование
В данном случае не нужно задумываться над своей системой логирования, имеющей целью выводить отчеты в своем формате. Нам нужно просто накапливать информацию о том, какой тестовый сценарий выполняется, и какие шаги уже пройдены. Соответственно, вся система логирования будет дополнена четырьмя функциями:
- StartTC - инициирует старт логирования для нового тестового сценария. Все внутренние переменные сбрасываются.
- EndTC - завершает логирование для текущего теcтового сценария
- IC - cообщает о начале блока реализации начальных условий теста
- Step - предшествует блоку реализации действий некоторого шага теста
Помимо этого, у нас должны быть некоторые переменные, которые будут хранить имя текущего теста, начальные условия, а также выполненные шаги с ожидаемыми результатами. Объявление переменных имеет вид:
var lsIC = new Array();
var lsSteps = new Array();
var lsExpected = new Array();
var curTcName;
Всё остальное - это реализация четырех вышеперечисленных функций. Они достаточно просты по своей структуре, поэтому не будем останавливаться на их описании. В конечном итоге файл _Logger содержит в себе следующее:
var lsIC = new Array();
var lsSteps = new Array();
var lsExpected = new Array();
var curTcName;
function StartTC( tcName ){
curTcName = tcName;
lsSteps = new Array();
lsExpected = new Array();
lsIC = new Array();
}
function EndTC( ){
curTcName = null;
}
function IC( description ){
lsIC.push( description );
}
function Step( description, expected ){
lsSteps.push( description );
lsExpected.push( expected );
}
Вот и всё, что нам дополнительно нужно было написать для логирования. Весь остальной вывод осуществляется с использованием стандартных средств.
Описание основного принципа занесения ошибок в Mantis
Итак, прежде чем реализовать связку TestComplete и Mantis, необходимо четко определить порядок действий. Основная проблема pзаключается в том, что дефекты должны создаваться автоматически. Далее человек определяет, что делать с созданным дефектом. Соответственно, если один и тот же дефект создается неоднократно, это не очень хорошо c учетом того, что тесты будут выполняться интенсивнее в сравнении с исправлением дефектов. Поэтому необходимо вначале искать существующие дефекты и создавать новые лишь при отсутствии подобных.
Другой немаловажный момент заключается в том, что существующий дефект может быть уже отмечен как исправленный или невоспроизводимый. Но если ошибка воспроизвелась, то этот дефект нужно поднять и изменить его статус. При этом желательно добавить комментарий, указывающий версию продукта, в которой данный дефект вновь воспроизвлся.
Если дефект новый, он должен содержать всю необходимую информацию. В частности, основные атрибуты - такие как Summary, Description, Steps to Reproduce, Priority.
Summary обычно содержит краткое описание дефекта, дающее базовое представление о выявленной проблеме. Это поле обычно ограничено по количеству символов (в разных стандартах длина поля составляет 50-70 символов). Поместим в этом поле имя теста, номер шага и текст ошибки. Сочетание этих трех элементов делает полученную строку достаточно уникальной, чтобы использовать ее в качестве критерия поиска существующих дефектов.
Description представляет собой более развернутое описание дефекта. В него можно включить непосредственно текст ошибки, а также добавить расширенное описание. В TestComplete для метода Log.Error есть несколько строковых параметров. В частности, второй параметр - это сообщение, которое в стандартном отчете TestComplete отображается как ремарка и будет видно лишь при выборе соответствующей строки в отчете. Таки образом, подобный параметр будет хорошим дополнением к тексту ошибки в качестве значения Description.
Steps to reproduce - одно из важнейших полей, позволяющее указать сценарий воспроизведения дефекта. Если эту информацию можно получить автоматически, то это более чем замечательно. О накоплении подобной информации мы позаботились при работе с файлом _Logger, в котором все методы так или иначе обрабатывают и сохраняют информацию о выполняемом тестовом сценарии и шаге выполнения.
Поле Prioritу имеет 6 значений. В стандартной системе логирования TestComplete каждое сообщение об ошибке имеет свой приоритет. В частности, приоритет можно выставить в третьем параметре метода Log.Error. Для него есть 5 зарезервированных значений - от наименьшего до требующего немедленного исправления. Эти приоритеты могут быть напрямую поставлены в соответствие приоритетам Mantis:
| Mantis Priority | TestComplete Priority |
|---|---|
| Immediate | pmHighest |
| Urgent | pmHigher |
| High | pmNormal |
| Normal | pmLower |
| Low | pmLowest |
| None | <Any other value> |
Как видим, обычное сообщение об ошибке, выданное TestComplete, по умолчанию соответствует дефекту с приоритетом High, что с точки зрения процесса трекинга соответствует нефатальной ошибке в работе тестируемого функционала. Действительно, выдача сообщения об ошибке не прекращает выполнение теста, но тем не менее проблема выявлена в некоторой точке верификации. Ничто не мешает выбрать другую схему, исходя из нужд конкретного проекта.
Итак, мы рассмотрели основные моменты, которые надо учесть при автоматическом создании дефекта. Теперь перейдем к реализации.
Пример теста и реализация создания дефектов
Рассмотрим типичный тест, для которого организуется подобная система. Откроем скрипт SampleTest и напишем тест, у которого есть всего два шага. Первый шаг просто выводит сообщение о выполнении этапа, а второй шаг выдает обычное сообщение об ошибке:
//USEUNIT _Logger
function SampleTest(){
StartTC( "SampleTest" );
try {
Step( "Pass the step" , "Step is passed" );
Log.Message( "Passed" );
Step( "Check Step" , "Step is passed" );
Log.Error( "Step failed" );
}
catch( e ) {
Log.Error( "Unhandled exception: " + e.number , e.description , pmHigher );
}
EndTC();
}
Прежде всего отметим структуру теста. Реализованные нами вспомогательные функции логирования выступают в роли фиксации текущего состояния выполнения. Тем не менее, они образуют каркас теста. Исполняемый код просто вписывается в нужные места.
Также стоит обратить внимание на обработчик исключений. При появлении исключения выдается достаточно краткое сообщение об ошибке. В ремарках будет больше деталей, что даст достаточно развернутый текст в Description дефекта. Приоритет будет установлен выше Normal. Зачастую подобные исключения следует воспринимать как ошибку реализации тестового скрипта, и от таких ошибок надо избавляться как можно скорее.
Как уже было указано выше, результатом работы теста будет обычное сообщение и сообщение об ошибке. Нас интересует второе.
При создании проекта в TestCompletе мы подготовили файл _Events, объект Events и обработчик для события OnLogError. В этом обработчике разместим весь необходимый код для создания дефектов в Mantis. Сейчас обработчик имеет вид:
//USEUNIT _Logger
function GeneralEvents_OnLogError( Sender, LogParams ){
}
На данном этапе нам надо сгенерировать строку summary по имени теста, номеру шага и тексту ошибки. Всё необходимое для данной операции мы можем получить. Осталось добавить переменную и проинициализировать её:
//USEUNIT _Logger
//USEUNIT _MantisUtils
function GeneralEvents_OnLogError( Sender, LogParams ){
var summary;
if( LogParams.StrEx == undefined ){
LogParams.StrEx = "";
}
summary = "ATC: " + curTcName + " Step: " + (lsSteps.length) + ": " + LogParams.Str;
}
Теперь пора обращаться к веб-сервисам. Нам нужно зарезервировать логин, пароль, а также ряд других параметров, необходимых при создании нового дефекта в Mantis. Для этого мы предварительно создали файл _MantisConfig. Теперь пора его заполнить содержимым: логин, пароль, имя проекта, имя компонента (категория), версия, билд, статус, приоритет и еще ряд других параметров. Объявим соответствующие переменные в файле _MantisConfig:
var userName;
var password;
var projectName;
var component;
var prjVersion;
var build;
var viewState;
var status;
var resolution;
var severity;
var priority;
var os;
Из всех вышеобъявленных переменных мы можем вычислить "на лету" только priority. Всё остальное должно быть задано явным образом. К этому файлу мы вернемся позже.
Возвращаемся к файлу _Events. Мы получили summary и зарезервировали переменные под различные параметры создаваемого дефекта. Теперь мы можем задать значение приоритета в Mantis, а затем произвести поиск существующего дефекта по заданному summary. Преобразуем обработчик сообщений об ошибке к виду:
//USEUNIT _Logger
//USEUNIT _MantisConfig
function GeneralEvents_OnLogError( Sender, LogParams ){
var summary;
var issueId;
if( LogParams.StrEx == undefined ){
LogParams.StrEx = "";
}
switch( LogParams.Priority ){
case pmLowest:
priority = "low";
break;
case pmLower:
priority = "normal";
break;
case pmNormal:
priority = "high";
break;
case pmHigher:
priority = "urgent";
break;
case pmHighest:
priority = "immediate";
break;
default:
priority = "none";
break;
}
summary = "ATC: " + curTcName + " Step: " + (lsSteps.length) + ": " + LogParams.Str;
issueId = WebServices.MantisWebService.mc_issue_get_id_from_summary( userName , password , summary.substr( 0 , 50 ) );
if( VarToInteger( issueId ) > 0 ){
// TODO Add existing issue update code
}
else {
// TODO Add new issue creation code
}
}
Жирным шрифтом подсвечены внесенные изменения. Во-первых, мы подключили _MantisConfig. Во-вторых, вычислили значение приоритета. В-третьих, произвели поиск дефектов по summary. И наконец, разделили выполнение на 2 ветки: обработку существующего дефекта и создание нового.
Несмотря на то, что в тексте рассматриваемого файла обработка существующего дефекта идет выше, вначале создадим сам дефект. Информация о создаваемом дефекте передается величиной типа IssueData. Это достаточно сложная структура, в которой некоторые поля тоже являются сложными типами. Как подготовить данную структуру? В TestComplete для каждого веб-сервиса есть элемент TypeFactory, у которого есть поля, соответствующие типам данных, определенным в веб-сервисе. Эти поля возвращают объект, содержащий все необходимые поля, которые определены в одноименном типе данных. То есть, строка вида
issueData = WebServices.MantisWebService.TypeFactory.IssueData;
вернет уже созданный объект, содержащий поля (с таким же именем), определенные в типе IssueData веб-сервиса MantisWebService. У полученного объекта есть несколько полей, которые отдельно надо объявить как величины соответствующих составных типов. Чтобы не совмещать инициализацию объекта для хранения данных дефекта с заполнением полей, вынесем подготовку самой величины в отдельную функцию. Для таких целей мы зарезервировали файл _MantisUtils, который имеет следующее содержимое:
//USEUNIT _MantisConfig
function prepareIssueData(){
issueData = WebServices.MantisWebService.TypeFactory.IssueData;
issueData.project = WebServices.MantisWebService.TypeFactory.ObjectRef;
issueData.view_state = WebServices.MantisWebService.TypeFactory.ObjectRef;
issueData.status = WebServices.MantisWebService.TypeFactory.ObjectRef;
issueData.resolution = WebServices.MantisWebService.TypeFactory.ObjectRef;
issueData.reporter = WebServices.MantisWebService.TypeFactory.AccountData;
issueData.severity = WebServices.MantisWebService.TypeFactory.ObjectRef;
issueData.priority = WebServices.MantisWebService.TypeFactory.ObjectRef;
issueData.reproducibility = WebServices.MantisWebService.TypeFactory.ObjectRef;
issueData.projection = WebServices.MantisWebService.TypeFactory.ObjectRef;
issueData.eta = WebServices.MantisWebService.TypeFactory.ObjectRef;
return issueData;
}
В результате работы этой функции у нас будет готовый объект типа IssueData с пустыми полями. Теперь применим эту функцию в нашем обработчике ошибок и заполним сформированную структуру данными.
Перейдем к файлу _Events и обновим его:
//USEUNIT _Logger
//USEUNIT _MantisUtils
function GeneralEvents_OnLogError( Sender, LogParams ){
var summary;
var issueId;
var issueData;
if( LogParams.StrEx == undefined ){
LogParams.StrEx = "";
}
switch( LogParams.Priority ){
case pmLowest:
priority = "low";
break;
case pmLower:
priority = "normal";
break;
case pmNormal:
priority = "high";
break;
case pmHigher:
priority = "urgent";
break;
case pmHighest:
priority = "immediate";
break;
default:
priority = "none";
break;
}
summary = "ATC: " + curTcName + " Step: " + (lsSteps.length) + ": " + LogParams.Str;
issueId = WebServices.MantisWebService.mc_issue_get_id_from_summary( userName , password , summary.substr( 0 , 50 ) );
if( VarToInteger( issueId ) > 0 ){
// TODO Add existing issue update code
}
else {
issueData = prepareIssueData();
issueData.project.name = projectName;
issueData.version = prjVersion;
issueData.build = build;
issueData.category = component;
issueData.summary = summary;
issueData.description = LogParams.Str + ".\r\n Ex: " + LogParams.StrEx
issueData.steps_to_reproduce = "- " + lsSteps.join( "\r\n- " ) +
"\r\nExpected:\r\n\t" + lsExpected[lsExpected.length-1] +
"\r\nActual:\r\n\t" + LogParams.Str;
issueData.view_state.name = viewState;
issueData.status.name = status;
issueData.resolution.name = resolution;
issueData.reporter.name = userName;
issueData.severity.name = severity;
issueData.priority.name = priority;
issueData.reproducibility.name = "N/A";
issueData.os = os;
WebServices.MantisWebService.mc_issue_add( userName , password , issueData );
}
}
Последние изменения снова подсвечены жирным шрифтом. Теперь вместо файла _MantisConfig мы подключаем _MantisUtils. Мы объявили переменную issueData, сформировали ее структуру путем вызова созданной функции prepareIssueData, заполнили подя структуры и вызвали функцию mc_issue_add веб-сервиса MantisWebService.
Отдельно хотелось бы обратить внимание на фрагмент, подсвеченный красным. В данном фрагменте генерируются значения полей Description и Steps To Reproduce. Значение первого поля получается путем соединения основного сообщения об ошибке и ремарки к нему. Значение второго поля получается путем склеивания всех описаний шагов (функция Step заполняет определенные списки) и строки с ожидаемым и фактическим результатом.
Теперь нам осталось обработать случай существования аналогичного дефекта. Для этого нам понадобятся две функции веб-сервиса: mc_issue_update для модификации статуса дефекта и mc_issue_note_add для добавления пометки о том, что дефект воспроизводится в текущей версии. В конечном варианте обработчик сообщения об ошибке выглядит так:
//USEUNIT _Logger
//USEUNIT _MantisUtils
function GeneralEvents_OnLogError( Sender, LogParams ){
var summary;
var issueId;
var issueData;
var note;
if( LogParams.StrEx == undefined ){
LogParams.StrEx = "";
}
switch( LogParams.Priority ){
case pmLowest:
priority = "low";
break;
case pmLower:
priority = "normal";
break;
case pmNormal:
priority = "high";
break;
case pmHigher:
priority = "urgent";
break;
case pmHighest:
priority = "immediate";
break;
default:
priority = "none";
break;
}
summary = "ATC: " + curTcName + " Step: " + (lsSteps.length) + ": " + LogParams.Str;
issueId = WebServices.MantisWebService.mc_issue_get_id_from_summary( userName , password , summary.substr( 0 , 50 ) );
if( VarToInteger( issueId ) > 0 ){
issueData = WebServices.MantisWebService.mc_issue_get( userName , password , issueId );
issueData.status = WebServices.MantisWebService.TypeFactory.ObjectRef;
issueData.status.name = status;
issueData.resolution = WebServices.MantisWebService.TypeFactory.ObjectRef;
issueData.resolution.name = resolution;
// Clear existing notes list
issueData.notes = WebServices.MantisWebService.TypeFactory.IssueNoteDataArray;
WebServices.MantisWebService.mc_issue_update( userName , password , issueId , issueData );
note = WebServices.MantisWebService.TypeFactory.IssueNoteData;
note.reporter = WebServices.MantisWebService.TypeFactory.AccountData;
note.reporter.name = userName;
note.text = "Defect is reproduced on version: " + prjVersion + "\r\n Environment: " + os;
WebServices.MantisWebService.mc_issue_note_add( userName , password , issueId , note );
}
else {
issueData = prepareIssueData();
issueData.project.name = projectName;
issueData.version = prjVersion;
issueData.build = build;
issueData.category = component;
issueData.summary = summary;
issueData.description = LogParams.Str + ".\r\n Ex: " + LogParams.StrEx
issueData.steps_to_reproduce = "- " + lsSteps.join( "\r\n- " ) +
"\r\nExpected:\r\n\t" + lsExpected[lsExpected.length-1] +
"\r\nActual:\r\n\t" + LogParams.Str;
issueData.view_state.name = viewState;
issueData.status.name = status;
issueData.resolution.name = resolution;
issueData.reporter.name = userName;
issueData.severity.name = severity;
issueData.priority.name = priority;
issueData.reproducibility.name = "N/A";
issueData.os = os;
WebServices.MantisWebService.mc_issue_add( userName , password , issueData );
}
}
Фрагмент кода, подсвеченный жирным шрифтом - это недостающая часть. Отдельно стоит обратить внимание на строчку:
// Clear existing notes list
issueData.notes = WebServices.MantisWebService.TypeFactory.IssueNoteDataArray;
После того, как мы получили информацию о существующем дефекте и поменяли ряд полей, нужно очистить различные массивы. В данном случае это относится только к Notes, но с таким же успехом применимао к другим наборам, например, attachments. Дело в том, что вызов функции mc_issue_update произведет модификацию полей простых типов, но если попадется массив, то все элементы этого массива будут добавлены. Чтобы избежать дублирования всех имеющихся элементов поля notes, перед обновлением очистим имеющийся список notes.
Переменные, объявленные в _MantisConfig пока что не инициализированы, поэтому в текущем виде пример не отработает. Но это будет сделано через ANT. А пока что весь Project Suite можно поместить в репозиторий. В нашем примере Project Suite называется Sample. Соответственно, он хранится в одноименном каталоге. В репозитории этот Suite хранится по адресу "${svn_repo}/Sample". После извлечения из SVN он будет находиться в "${working_dir}/Sample".
Подключение TestComplete к ANT
Поскольку в основе выполнения всего разрабатываемого конвейера лежит ANT, нужно реализовать вызов TestComplete через ANT. Путь решения данный проблемы один - вызов из командной строки. Но вот реализаций может быть несколько. В ANT есть задача, выполняющая командную строку. Но это универсальная задача, и инструкции по запуску TestComplete в данном случае будут слишком громоздкими. Поэтому лучше создать свою ANT-задачу, которая будет запускать TestComplete и выполнять указанную функцию для нашего проекта. Заодно это полезно для ознакомления с созданием ANT-задачи.
О создании своей задачи для ANT можно прочитать здесь. Основная идея: нужно создать класс, который наследуется от класса org.apache.tools.ant.Task или любого его наследника. Атрибуты задачи соответствуют свойствам создаваемого класса и присваиваются c помощью set-методов, определенных для каждого свойства. В данном случае у нас просто командная строка со своими параметрами и достаточно будет разработать задачу, основные параметры которой заданы в атрибутах, а не в дочерних узлах.
Задачу разработаем на Java, поэтому нам нужем любой IDE для Java, например, Eclipse. Для написания данного примера создан новый Java-проект, к которому в качестве внешней библиотеки подключен файл Ant.jar, находящийся в каталоге <ANT_dir>/lib. Чтобы сделать пример переносимым, данный файл был скопирован в наш рабочий каталог в папку lib. В дальнейшем мы будем ссылаться именно на этот файл. Вернемся к Java-проекту. Реализация данной задачи помещена в package с именем com.atsg.ant.tasks, в котором был создан класс TestCompleteTask. Приведем полный код данного класса:
/**
* @(#)TestCompleteTask.java 03 Jan 2009
*/
package com.atsg.ant.tasks;
import java.util.ArrayList;
import org.apache.tools.ant.Task;
/**
* @author KaNoN
* @version 1.0
*/
public class TestCompleteTask extends Task {
private boolean exec = false;
private String projectSuite = "";
private String project = "";
private String projectitem = "";
private String unit = "";
private String routine = "";
private boolean exit = false;
private boolean silentmode = false;
private boolean nosplash = false;
public void setExec( String execute ){
this.exec = execute.equals( "true" );
}
/**
* @param projectSuite the projectSuite to set
*/
public void setProjectSuite(String projectSuite) {
this.projectSuite = projectSuite;
}
/**
* @param project the project to set
*/
public void setProject(String project) {
this.project = project;
}
/**
* @param projectitem the projectitem to set
*/
public void setProjectitem(String projectitem) {
this.projectitem = projectitem;
}
/**
* @param unit the unit to set
*/
public void setUnit(String unit) {
this.unit = unit;
}
/**
* @param routine the routine to set
*/
public void setRoutine(String routine) {
this.routine = routine;
}
/**
* @param exit the exit to set
*/
public void setExit(String exit) {
this.exit = exit.equals("true");
}
/**
* @param silentmode the silentmode to set
*/
public void setSilentmode(String silentmode) {
this.silentmode = silentmode.equals( "true" );
}
/**
* @param nosplash the nosplash to set
*/
public void setNosplash(String nosplash) {
this.nosplash = nosplash.equals( "true" );
}
public void execute() {
try {
Runtime rt = Runtime.getRuntime();
ArrayListparams = new ArrayList ();
if( !this.exec ){
params.add("TestComplete.exe");
}
else {
params.add("TestExecute.exe");
}
if( !this.projectSuite.equals( "" ) ){
params.add( this.projectSuite );
}
if( !this.project.equals( "" )){
params.add( "/r" );
params.add( "/project:" + this.project );
}
if( !this.projectitem.equals( "" ) ){
params.add( "/projectitem:" + this.projectitem );
}
if( !this.unit.equals( "" ) ){
params.add( "/unit:" + this.unit );
}
if( !this.routine.equals( "" ) ){
params.add( "/routine:" + this.routine );
}
if( this.exit ){
params.add( "/exit" );
}
if( this.silentmode ){
params.add( "/SilentMode" );
}
if( this.nosplash ){
params.add( "/nosplash" );
}
String cmdArray[] = new String [params.size()];
cmdArray = params.toArray( cmdArray );
Process ps = rt.exec( cmdArray );
ps.waitFor();
}
catch( Exception e ){
e.printStackTrace(System.err);
}
}
/**
*
*/
public TestCompleteTask() {
}
}
После этого наш Java-проект экспортируется в виде jar-файлa testCompleteAnt.jar в подкаталог lib папки, в которой находится build.xml
Теперь приступим к редактированию нашего ANT-скрипта. Первым делом сделаем ссылки на необходимые jar-файлы. Это testCompleteTask.jar и ant.jar, который находится в каталоге библиотек ANT. Модифицированный build.xml принимает вид:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="build" name="SampleIntegration">
<property file="config.properties"/>
<path id="project.classpath">
<pathelement location="./lib/svnjavahl.jar" />
<pathelement location="./lib/svnant.jar" />
<pathelement location="./lib/svnClientAdapter.jar" />
</path>
<path id="tc.classpath">
<pathelement location="${ant.home}/lib/ant.jar" />
<pathelement location="./lib/testCompleteAnt.jar" />
</path>
<target name="testCleanup">
<delete dir="${working_dir}" />
<mkdir dir="${working_dir}" />
</target>
<target name="testGetFiles" depends="testCleanup">
<taskdef name="svn" classpathref="project.classpath"
classname="org.tigris.subversion.svnant.SvnTask" />
<svn username="${svn_user}" password="${svn_pass}" svnkit="true">
<checkout url="${svn_repo}" destPath="${working_dir}" />
</svn>
</target>
</project>
Внесенные изменения подсвечены жирным шрифтом. Обратите внимание на путь к Ant.jar. В самом ANT есть ряд зарезервированных переменных. Одна из них, именуемая ant.home, как раз указывает на расположение каталога ANT. Соответственно, если нам надо сослаться на стандартные ANT-овские библиотеки, то нам как раз нужно воспользоваться данной переменной. Но это только определение путей. Теперь опишем сами задачи. Это отдельный target, который будет выполняться сразу после testGetFiles. Содержимое ANT-скрипта:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="build" name="SampleIntegration">
<property file="config.properties"/>
<path id="project.classpath">
<pathelement location="./lib/svnjavahl.jar" />
<pathelement location="./lib/svnant.jar" />
<pathelement location="./lib/svnClientAdapter.jar" />
</path>
<path id="tc.classpath">
<pathelement location="${ant.home}/lib/ant.jar" />
<pathelement location="./lib/testCompleteAnt.jar" />
</path>
<target name="testCleanup">
<delete dir="${working_dir}" />
<mkdir dir="${working_dir}" />
</target>
<target name="testGetFiles" depends="testCleanup">
<taskdef name="svn" classpathref="project.classpath"
classname="org.tigris.subversion.svnant.SvnTask" />
<svn username="${svn_user}" password="${svn_pass}" svnkit="true">
<checkout url="${svn_repo}" destPath="${working_dir}" />
</svn>
</target>
<target name="build" depends="testGetFiles">
<taskdef name="tc" classpathref="tc.classpath"
classname="com.atsg.ant.tasks" />
<tc projectSuite="${working_dir}/Sample/Sample.pjs"
project="SampleWebService"
unit="SampleTest"
routine="SampleTest"
exit="true"
nosplash="true"
/>
</target>
</project>
Обратите внимание на фрагмент, подсвеченный жирным шрифтом. Здесь мы определяем задачу tc и задаем запуск TestComplete с указанием project suite, проекта и функции. Также, здесь мы указали, что TestComplete надо закрыть по завершении работы и не отображать различные splash-окна.
У нас почти всё готово. Почти. В ходе работы с TestComplete мы создали файл _MantisConfig, в котором были объявлены основные параметры, но они не были проинициализированы. Задавать их явно было неразумно, так как было неизвестно, с какими параметрами мы будем запускать тесты. В любом случае, конфигурация тестов должна настраиваться только в одном месте, а для этого у нас есть файл config.properties. Но для нормальной работы TestComplete нужно, чтобы переменные в _MantisConfig были проинициализированы. Для этого нам надо пойти на хитрость. Мы знаем, какие переменные должны быть проинициализированы и какие значения им надо присвоить (они заданы в config.properties). Соответственно, строки инициализации нужно добавить в конец файла _MantisConfig. То есть, нам нужно дописать несколько строк в конец файла. Для таких случаев в ANT есть задача concat, добавляющая строку в конец файла. В итоге, build.xml приобретает вид:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="build" name="SampleIntegration">
<property file="config.properties"/>
<path id="project.classpath">
<pathelement location="./lib/svnjavahl.jar" />
<pathelement location="./lib/svnant.jar" />
<pathelement location="./lib/svnClientAdapter.jar" />
</path>
<path id="tc.classpath">
<pathelement location="${ant.home}/lib/ant.jar" />
<pathelement location="./lib/testCompleteAnt.jar" />
</path>
<target name="testCleanup">
<delete dir="${working_dir}" />
<mkdir dir="${working_dir}" />
</target>
<target name="testGetFiles" depends="testCleanup">
<taskdef name="svn" classpathref="project.classpath"
classname="org.tigris.subversion.svnant.SvnTask" />
<svn username="${svn_user}" password="${svn_pass}" svnkit="true">
<checkout url="${svn_repo}" destPath="${working_dir}" />
</svn>
</target>
<target name="build" depends="testGetFiles">
<property name="tc_config" value="${working_dir}/Sample/SampleWebService/Script/_MantisCongig.sj" />
<concat destfile="${tc_config}" append="true">userName = "${mantis_user}";</concat>
<concat destfile="${tc_config}" append="true">password = "${mantis_pass}";</concat>
<concat destfile="${tc_config}" append="true">projectName = "${mantis_prj}";</concat>
<concat destfile="${tc_config}" append="true">component = "${mantis_component}";</concat>
<concat destfile="${tc_config}" append="true">prjVersion = "${mantis_prjver}";</concat>
<concat destfile="${tc_config}" append="true">build = "${mantis_build}";</concat>
<concat destfile="${tc_config}" append="true">viewState = "${mantis_view_state}";</concat>
<concat destfile="${tc_config}" append="true">status = "${mantis_status}";</concat>
<concat destfile="${tc_config}" append="true">resolution = "${mantis_resolution}";</concat>
<concat destfile="${tc_config}" append="true">severity = "${mantis_severity}";</concat>
<concat destfile="${tc_config}" append="true">os = "${mantis_os}";</concat>
<taskdef name="tc" classpathref="tc.classpath"
classname="com.atsg.ant.tasks" />
<tc projectSuite="${working_dir}/Sample/Sample.pjs"
project="SampleWebService"
unit="SampleTest"
routine="SampleTest"
exit="true"
nosplash="true"
/>
</target>
</project>
Отметим, что target, отвечающий за выполнение тестов в TestComplete, назван build - точно также, как и атрибут default у задачи project (посмотрите текст файла build.xml). Это означает, что данный target будет выполняться по умолчанию.
Осталось запустить данное решение. Убедитесь, что все необходимые приложения запущены, и все параметры конфигурации заданы верно. После этого достаточно зайти в каталог, в котором содержится build.xml, и в командной строке набрать:
- ant testCleanup - выполнит очистку рабочего каталога
- ant testGetFiles - выполнит очистку рабочего каталога и извлечет файлы
- ant - выполнит очистку рабочего каталога, извлечет файлы и выполнит тесты TestComplete
Заключение
Данный пример демонстрирует возможность интеграции между системами, которые изначально не связаны между собой. По аналогии можно подготовить решения для других сочетаний систем багтрекинга, систем автоматизированного тестирования и контроля версий. Подобная связка легко вписывается в общую систему автоматической сборки. Настроив подобную цепочку, вы сможете автоматизировать целый ряд рутинных задач: подготовка среды, извлечение последних версий файлов, запуск тестов и публикация дефектов. Помимо этого, технологическая цепочка, представленная в данной статье, может применяться для локальных ежедневных запусков. Такая схема позволит удостовериться, что уже написанные тесты работают стабильно, или же оперативно сообщить подробную информацию о дефектах. Таким образом, некоторую часть процесса контроля качества мы полностью перенесли на машину.
