AT.info ПОСИДЕЛКИ  vKontakte   facebook группа  
Ищешь как решить проблему с Selenium?! Спроси людей. Они все знают!
view counter
Очень хочется узнать его. Форум ждет тебя!
view counter
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 - непосредственно значение атрибута, по которому ищется объект

То есть одна строка идентифицирует объект. У подобного решения есть одно достаточно сильное преимущество - простота использования. Но подобная строка не отражает логического смысла объекта. Например, локатор

xpath=//img[@alt=‘The image alt text’]

Позволит определить, какой объект реально искать на форме, но совсем непонятно, какая форма должна быть. То есть с точки зрения удобства чтения мы не в состоянии фиксировать, на какой странице находится данная ссылка, достаточно сложно выявить логический смысл самой ссылки ( а это важно, так как при чтении кода больше упор ведется на логический смысл нежели фактические атрибуты ). Соответственно, удобно было бы сделать псевдоним вида: 

<Псевдоним страницы>.<Псевдоним объекта>

просто для удобства чтения. Для этого можно сделать классы-обертки вида:

class MyPage

     def lnkLink()
          "xpath=//img[@alt='The image alt text']"
     end

end

После чего мы можем создать экземпляр данного класса:

wTestPage = MyPage.new

и вместо локатора использовать выражение вида:

wTestPage.lnkLink

Уже проще, так как в случае модификации атрибутов ссылки нам не надо будет менять локаторы во всех тестах, достаточно будет внести корректировки в объявлении класса. Также это решение удобно тем, что оконные декларации - это такая же часть програмного кода, что и непосредственно реализации тестов. Но тем не менее, немного неудобно нагромождать большое количество подобных классов, а если тестируемое приложение содержит много страниц, то классов будет много, что влечет за собой большое количество файлов. Поэтому, зачастую целесообразно отделить ресурсы ( оконные декларации ) от движка ( непосредственно програмной реализации ). В качестве аналога можно вспомнить NameMapping в TestComplete. Файл, описывающий правила маппинга - это XML-файл. Соответственно, можно попробовать сделать аналогичную реализацию на Ruby. Тем более в данном случае задача заметно проще, так как практически нет иерархии объектов, а сами объекты описываются одной строкой, а не множеством атрибутов.

Итак, отдельную страницу с её элементами можно описать в виде XML-файла, например, вот так:

//img[@alt=‘The image alt text’]

Каждый узел page содержит имя страницы и текст заголовка ( например, по заголовку мы можем идентифицировать, действительно ли именно эта страница сейчас открыта ). Каждый узел item содержит имя и тип локатора, а значение между открывающимся и закрывающимся тегами - непосредственно значение локатора.

Итак, для начала разберемся, как мы будем читать XML-файл. Вначале мы считаем содержимое файла в некоторую строку:

src = "c:/temp/test.xml" # Specify any other path to existing XML file
	xml_data = ""
	IO.foreach( src ) { |line| xml_data = xml_data + line }

Теперь xml_data содержит содержимое файла, имя которого мы задали в переменной src. Этот текст осталось только распарсить. Для этих целей мы воспользуемся классом REXML::Document. Выглядит это примерно так:

require 'rexml/document'
	....
	doc = REXML::Document.new(xml_data)

В результате, в переменной doc у нас содержится структура XML-файла, которую мы можем уже обрабатывать. В частности, мы можем пройтись по всем элементам page

doc.elements.each('/page') do |elem|
		# do something for each element stored in elem variable
	end

Аналогичный цикл надо провести по всем элементам /page/item для каждой страницы. И все это заправить под один общий класс. Выглядит это примерно так:

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

А теперь рассмотрим, что же можно сделать дальше. У нас в XML файле есть узел page, атрибут name которого имеет значение wTestPage. А у него уже есть элемент lnkLink. То есть, в коде хотелось бы получить что-то типа:

$testMap = ObjMapping.new( "c:/temp/test.xml" )
locator = $testMap.wTestPage.lnkLink

В последней строке кода мы должны получить строку локатора. То есть было бы неплохо к объекту для маппинга добавить объект для хранения данных страницы со всеми ее элементами. Для этого нам нужен еще один класс, который будет отвечать за хранение данных отдельной страницы. У каждой страницы есть атрибут title и метод exists?, определяющий, что данная страница открыта. Таким образом, получаем общую реализацию в виде:

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

Выделенный фрагмент представляет наибольший интерес. Здесь мы динамически конструируем экземпляр класса ObjMapping. Фактически, мы считываем значение атрибута name для узла page, а затем добавляем метод, который либо создаст свойство с таким же именем, либо вернет его значение. Допустим, у узла page атрибут name имеет значение "wTestPage", а атрибут title установлен в "Test Page". Соответственно, нужно добавить метод вида:

def wTestPage()
	if( !self.instance_variable_defined( @wTestPage ) )
		@wTestPage = PageClass().new( "Test Page" )
	else
		@wTestPage
	end
end

И точно так же для других page-узлов. То есть каркас тот же самый, варьируются только имена. Соответственно, мы формируем строку, содержащую код подобного метода, параметризируя соответствующие варьируемые значения, а затем вызываем метод instance_eval, который строку, передаваемую параметром выполнит для конкретного экземпляра данного класса. В данном случае это означает, что мы динамически добавили новый метод к обрабатываемому экземпляру класса ObjMapping. То есть, если мы просто создадим новый экземпляр данного класса, то добавленного выше метода в нем не будет.

И теперь у нас остался еще один цикл, перебирающий элементы узла page. То есть аналогичным образом надо подобавлять методы в экземпляр класса PageClass. И в конечном итоге получаем код вида:

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

результате, имея XML-файл вида (допустим полное имя файла - "c:/temp/test.xml" ):

//img[@alt=‘The image alt text’]

а также вышеприведенную реализацию ObjMapping класса, мы можем получить локатор, используя выражения вида:

$testMap = ObjMapping.new( "c:/temp/test.xml" )
locator = $testMap.wTestPage.lnkLink

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

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

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

© 2009-2010 Портал для автоматизаторов тестирования ПО
Автор проекта Поляруш Михаил | При использовании материалов ссылка на www.automated-testing.info обязательна.
Все замечания и пожелания присылайте на webmaster@automated-testing.info.
теплый пол киев
Яндекс.Метрика