Selenium RC (Ruby): Вынесение оконных деклараций в XML-файл

Одной из наиболее серьезных проблем при автоматизации функционального тестирования на уровне 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}

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

  1. При изменении значений локаторов достаточно внести корректировки только во внешних ресурсах
  2. Есть возможность структурировать объекты по страницам и подгружать только те страницы, которые нужны для конкретного теста
  3. Четкое отделение движка от ресурсов, позволяющее быстро локализовать проблему
  4. Приведя XML-описание страниц к некоторому фиксированному формату, можно портировать подобные решения даже на другие решения по автоматизации тестирования подобного рода ( например, на Ruby написана библиотека Watir).

К сожалению, для языков компилируемого типа (C, C++, Java ) подобное решение в том же самом виде неприменимо, так как компилятор заранее не знает, что некоторые объекты будут собраны по ходу выполнения тестов. Тем не менее, аналогичные механизмы в языках подобного вида могут быть реализованы путем хранения значений в структурах данных типа “словарь” и предоставления интерфейсов для извлечения конкретных значений. В любом случае, есть возможность заменить явное объявление локатора некоторым выражением, которое вернет этот локатор.