Одной из наиболее серьезных проблем при автоматизации функционального тестирования на уровне GUI является высокая чувствительность тестов к изменениям GUI. По-хорошему, подобная ситуация не должна возникать, так как автоматизация подобного рода ставится тогда, когда пользовательский интерфейс более-менее стабилен. Но это идеальная ситуация. В реальности, продукт меняется по всем направлениям и в том числе это касается пользовательского интерфейса. Так или иначе какие-то мелкие изменения имеют место (поле переименовали, переместили, поменяли некоторые идентификаторы) и это уже влияет на работоспособность тестов. Частично, можно реализовать гибкий механизм поиска объектов, на который подобные изменения не повлияют, но в большинстве случаев корректировок самих реализаций тестов не избежать. Соответственно, надо как-то минимизировать трудозатраты на корректировку. Наиболее эффективным решением данной проблемы можно назвать вынесение оконных деклараций во внешний ресурс и использование "псевдонимов". Подобное реализовано в WinRunner ( GUI Map ), QTP, RFT ( в котором есть возможность маппинга и все оконные объекты можно классифицировать как mappable и non-mappable ), TestComplete ( NameMapping и Alias, появившийся в поздних версиях ). Суть подобных решений в том, чтобы некоторому оконному объекту с заданными атрибутами поставить в соответствие некоторое имя, которое и будет использовано для обращения к данному оконному объекту.
Во многих средствах подобный механизм реализован, но существует много различных решений, которые фактически представляют собой некоторую библиотеку с прикрученным тестовым движком. В этом случае приходится пользоваться возможностями языка, на котором эти тесты пишутся. В качестве примера рассмотрим язык Ruby и конкретно его порт под Selenium RC. Почему взят именно Ruby? Во-первых, на Ruby есть еще несколько решений аналогичных Selenium RC и возможности языка, там применимы в той же мере. Во-вторых, Ruby - один из примеров языков интерпретируемого типа, у которого есть возможность динамического формирования и компоновки объектов. Подобные механизмы имеются и во многих других скриптовых языках ( в частности JavaScript ), поэтому Ruby был выбран в качестве демонстрации самой возможности подобной компоновки. На других языках подобные решения реализуются по аналогии с поправкой на специфику.
Как известно, в Selenium оконные объекты распознаются с помощью локаторов - специальных строк вида:
<how>=<value>, где
how - определяет атрибут, по которому ищется объект. Это может быть id,name, dom, xpath и многие другие ( в документации по Selenium о локаторах достаточно много расписано )
value - непосредственно значение атрибута, по которому ищется объект
То есть одна строка идентифицирует объект. У подобного решения есть одно достаточно сильное преимущество - простота использования. Но подобная строка не отражает логического смысла объекта. Например, локатор
{syntaxhighlighter brush: xml;fontsize: 100; first-line: 1; }xpath=//img[@alt=‘The image alt text’]{/syntaxhighlighter}
Позволит определить, какой объект реально искать на форме, но совсем непонятно, какая форма должна быть. То есть с точки зрения удобства чтения мы не в состоянии фиксировать, на какой странице находится данная ссылка, достаточно сложно выявить логический смысл самой ссылки ( а это важно, так как при чтении кода больше упор ведется на логический смысл нежели фактические атрибуты ). Соответственно, удобно было бы сделать псевдоним вида:
<Псевдоним страницы>.<Псевдоним объекта>
просто для удобства чтения. Для этого можно сделать классы-обертки вида:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }class MyPage
def lnkLink()
"xpath=//img[@alt='The image alt text']"
end
end{/syntaxhighlighter}
После чего мы можем создать экземпляр данного класса:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }wTestPage = MyPage.new{/syntaxhighlighter}
и вместо локатора использовать выражение вида:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }wTestPage.lnkLink{/syntaxhighlighter}
Уже проще, так как в случае модификации атрибутов ссылки нам не надо будет менять локаторы во всех тестах, достаточно будет внести корректировки в объявлении класса. Также это решение удобно тем, что оконные декларации - это такая же часть програмного кода, что и непосредственно реализации тестов. Но тем не менее, немного неудобно нагромождать большое количество подобных классов, а если тестируемое приложение содержит много страниц, то классов будет много, что влечет за собой большое количество файлов. Поэтому, зачастую целесообразно отделить ресурсы ( оконные декларации ) от движка ( непосредственно програмной реализации ). В качестве аналога можно вспомнить NameMapping в TestComplete. Файл, описывающий правила маппинга - это XML-файл. Соответственно, можно попробовать сделать аналогичную реализацию на Ruby. Тем более в данном случае задача заметно проще, так как практически нет иерархии объектов, а сами объекты описываются одной строкой, а не множеством атрибутов.
Итак, отдельную страницу с её элементами можно описать в виде XML-файла, например, вот так:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }//img[@alt=‘The image alt text’]{/syntaxhighlighter}
Каждый узел page содержит имя страницы и текст заголовка ( например, по заголовку мы можем идентифицировать, действительно ли именно эта страница сейчас открыта ). Каждый узел item содержит имя и тип локатора, а значение между открывающимся и закрывающимся тегами - непосредственно значение локатора.
Итак, для начала разберемся, как мы будем читать XML-файл. Вначале мы считаем содержимое файла в некоторую строку:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }src = “c:/temp/test.xml” # Specify any other path to existing XML file
xml_data = “”
IO.foreach( src ) { |line| xml_data = xml_data + line }{/syntaxhighlighter}
Теперь xml_data содержит содержимое файла, имя которого мы задали в переменной src. Этот текст осталось только распарсить. Для этих целей мы воспользуемся классом REXML::Document. Выглядит это примерно так:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }require ‘rexml/document’
…
doc = REXML::Document.new(xml_data){/syntaxhighlighter}
В результате, в переменной doc у нас содержится структура XML-файла, которую мы можем уже обрабатывать. В частности, мы можем пройтись по всем элементам page
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }doc.elements.each(‘/page’) do |elem|
# do something for each element stored in elem variable
end{/syntaxhighlighter}
Аналогичный цикл надо провести по всем элементам /page/item для каждой страницы. И все это заправить под один общий класс. Выглядит это примерно так:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }require ‘rexml/document’
class ObjMapping
# Initializes Mapping object instance and appends it with data from
# XML file ( if specified )
def initialize( src = "" )
if( src != "" )
self.loadPages( src )
end
end
# Appends Mapping object instance with page definitions from XML file
# specified by src parameter
def loadPages( src )
xml_data = ""
IO.foreach( src ) { |line| xml_data = xml_data + line }
doc = REXML::Document.new(xml_data)
doc.elements.each('/page') do |page|
# do something for each page node
end
end
end{/syntaxhighlighter}
А теперь рассмотрим, что же можно сделать дальше. У нас в XML файле есть узел page, атрибут name которого имеет значение wTestPage. А у него уже есть элемент lnkLink. То есть, в коде хотелось бы получить что-то типа:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }$testMap = ObjMapping.new( “c:/temp/test.xml” )
locator = $testMap.wTestPage.lnkLink{/syntaxhighlighter}
В последней строке кода мы должны получить строку локатора. То есть было бы неплохо к объекту для маппинга добавить объект для хранения данных страницы со всеми ее элементами. Для этого нам нужен еще один класс, который будет отвечать за хранение данных отдельной страницы. У каждой страницы есть атрибут title и метод exists?, определяющий, что данная страница открыта. Таким образом, получаем общую реализацию в виде:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }require ‘rexml/document’
class PageClass
def initialize( new_title )
@title = new_title
end
def exists?()
# TODO: bind to SeleniumDriver class
end
end
class ObjMapping
# Initializes Mapping object instance and appends it with data from
# XML file ( if specified )
def initialize( src = "" )
if( src != "" )
self.loadPages( src )
end
end
# Appends Mapping object instance with page definitions from XML file
# specified by src parameter
def loadPages( src )
xml_data = ""
IO.foreach( src ) { |line| xml_data = xml_data + line }
doc = REXML::Document.new(xml_data)
doc.elements.each('/page') do |page|
name = page.attribute( "name" ).value
title = page.attribute( "title" ).value
new_def = "def " + name + "() " +
"( !self.instance_variable_defined?( \"@" + name + "\") )?" +
"(@" + name + " = PageClass.new( \"" + title + "\") ):" +
"(@" + name + ")" +
" end"
self.instance_eval( new_def )
page.elements.each() do |item|
# TO DO: do something for each page item
end
end
end
end
{/syntaxhighlighter}
Выделенный фрагмент представляет наибольший интерес. Здесь мы динамически конструируем экземпляр класса ObjMapping. Фактически, мы считываем значение атрибута name для узла page, а затем добавляем метод, который либо создаст свойство с таким же именем, либо вернет его значение. Допустим, у узла page атрибут name имеет значение “wTestPage”, а атрибут title установлен в “Test Page”. Соответственно, нужно добавить метод вида:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }def wTestPage()
if( !self.instance_variable_defined( @wTestPage ) )
@wTestPage = PageClass().new( “Test Page” )
else
@wTestPage
end
end{/syntaxhighlighter}
И точно так же для других page-узлов. То есть каркас тот же самый, варьируются только имена. Соответственно, мы формируем строку, содержащую код подобного метода, параметризируя соответствующие варьируемые значения, а затем вызываем метод instance_eval, который строку, передаваемую параметром выполнит для конкретного экземпляра данного класса. В данном случае это означает, что мы динамически добавили новый метод к обрабатываемому экземпляру класса ObjMapping. То есть, если мы просто создадим новый экземпляр данного класса, то добавленного выше метода в нем не будет.
И теперь у нас остался еще один цикл, перебирающий элементы узла page. То есть аналогичным образом надо подобавлять методы в экземпляр класса PageClass. И в конечном итоге получаем код вида:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; highlight: [9,10,11,12,13,14,15,16,17,18,19,20]; }require ‘rexml/document’
class PageClass
def initialize( new_title )
@title = new_title
end
def loadItem( node )
name = node.attribute( "name" ).value
locator = node.attribute( "how" ).value + "=" + node.get_text.value
new_def = "def " + name + "() " +
"( !self.instance_variable_defined?( \"@" + name + "\") )?" +
"(@" + name + " = \"" + locator + "\" ):" +
"(@" + name + ")" +
" end"
self.instance_eval( new_def )
end
def exists?()
# TODO: bind to SeleniumDriver class
end
end
class ObjMapping
# Initializes Mapping object instance and appends it with data from
# XML file ( if specified )
def initialize( src = "" )
if( src != "" )
self.loadPages( src )
end
end
# Appends Mapping object instance with page definitions from XML file
# specified by src parameter
def loadPages( src )
xml_data = ""
IO.foreach( src ) { |line| xml_data = xml_data + line }
doc = REXML::Document.new(xml_data)
doc.elements.each('/page') do |page|
name = page.attribute( "name" ).value
title = page.attribute( "title" ).value
new_def = "def " + name + "() " +
"( !self.instance_variable_defined?( \"@" + name + "\") )?" +
"(@" + name + " = PageClass.new( \"" + title + "\") ):" +
"(@" + name + ")" +
" end"
self.instance_eval( new_def )
page.elements.each() do |item|
(eval( "@" + name )).loadItem( item )
end
end
end
end{/syntaxhighlighter}
результате, имея XML-файл вида (допустим полное имя файла - “c:/temp/test.xml” ):
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }//img[@alt=‘The image alt text’]{/syntaxhighlighter}
а также вышеприведенную реализацию ObjMapping класса, мы можем получить локатор, используя выражения вида:
{syntaxhighlighter brush: ruby;fontsize: 100; first-line: 1; }$testMap = ObjMapping.new( “c:/temp/test.xml” )
locator = $testMap.wTestPage.lnkLink{/syntaxhighlighter}
Соответственно, непосредственно в коде тестов мы используем псевдонимы локаторов вместо явно заданных значений. Это дает нам ряд преимуществ:
- При изменении значений локаторов достаточно внести корректировки только во внешних ресурсах
- Есть возможность структурировать объекты по страницам и подгружать только те страницы, которые нужны для конкретного теста
- Четкое отделение движка от ресурсов, позволяющее быстро локализовать проблему
- Приведя XML-описание страниц к некоторому фиксированному формату, можно портировать подобные решения даже на другие решения по автоматизации тестирования подобного рода ( например, на Ruby написана библиотека Watir).
К сожалению, для языков компилируемого типа (C, C++, Java ) подобное решение в том же самом виде неприменимо, так как компилятор заранее не знает, что некоторые объекты будут собраны по ходу выполнения тестов. Тем не менее, аналогичные механизмы в языках подобного вида могут быть реализованы путем хранения значений в структурах данных типа “словарь” и предоставления интерфейсов для извлечения конкретных значений. В любом случае, есть возможность заменить явное объявление локатора некоторым выражением, которое вернет этот локатор.