2.3 Осуществление базовых операций

Назад Содержание Дальше

Итак, приступаем к написанию кода тесткейса. Тесткейс условно разобъем на 2 секции:

  • Объявление переменных – здесь объявляются все переменные
  • Реализация сценария – здесь непосредственно пишется код

В данном подпункте реализуем первые 6 шагов нашего сценария. Пока что никаких переменных не планируется, поэтому в первой секции оставим только комментарий вида://TODO: Add variables declaration here и пойдем дальше.

 

Помимо непосредственно действий скрипт также должен осуществлять какой-то вывод, чтоб фиксировать насколько успешно пройден скрипт, на какой стадии выявлены ошибки и какие они. Попросту нужно записывать начало и завершение некоторой стадии сценария, делать трассировку. Так что будем выводить описания шагов и ожидаемых результатов. Итак, первый шаг выглядит следующим образом:

Code

[ ]use "TestApp.inc"
[ ]
[+]testcase TestApp_Menu() appstate apsTestApp
	[ ]
	[ ]//TODO: Add variables declaration here
	[ ]
	[ ]Print("Step 1: From Main menu select File > New")
	[ ]wTestApp.SetActive()
	[ ]wTestApp.File.New.Pick()
	[+]if( !wChild.Exists() )
		[ ] LogError("No MDI Child Window #1 appears")
		[ ] return
	[+]else
		[ ]Print(" MDI Child Window #1 appears")

 

Поскольку это первый шаг, то рассмотрим его детально, чтоб потом не останавливаться. Итак, вначале мы активизировали главное окно, чтоб оно было в самом верху, для чего задействовали метод SetActive(). Данный метод действует для окон классов DialogBox, MainWin и его нужно задействовать до того, как приступить к другим манипуляциям с этим окном. В противном случае может возникнуть целый ряд ненужных ошибок. Так что использование SetActive для главного окна до манипуляций с объектами окна следует взять за правило. Аналогом этого действия для веб-приложений будет вызов Browser.SetActive().
wTestApp.File.New.Pick() выбирает пункт меню. Это стандартный метод класса меню.
if( !wChild.Exists() ) – проверяем окно на существование. Метод Exists() определен в AnyWin классе и используется всеми классами-наследниками данного класса. Этот метод возвращает истинное значение, если окно существует. У данного метода есть опциональный параметр – время, в течение которого нужно ждать появления окна. Это особенно важно для приложений с клиент-серверной архитектурой, когда клиенту еще нужно ждать ответа от сервера. Если время ожидания не указано, то будет использовано время из настроек агента (Options > Agent на вкладке Timing параметр Window Timeout ).
LogError(“No MDI Child Window #1 appears”) – стандартная функция SilkTest-а для выдачи сообщения об ошибке. Визуально это текст красного цвета. Также SilkTest учитывает число вхождений данных сообщений в отчет с целью ведения статистики ошибок, допущенных в ходе выполнения тесткейсов. Ошибки бывают критичными (дальнейшее выполнение невозможно, нецелесообразно или нет четкого определенного сценария дальнейших действий с целью выравнивания хода выполнения скрипта) или некритичными (ошибки, которые не влияют на дальнейший ход выполнения скрипта). В данном случае ошибка является критичной, так как 2-й шаг (в случае невыполнения первого) также будет выполнен некорректно, что вызывает цепное падение следующих 2-х шагов. Поэтому в данном случае происходит экстренное завершение выполнения тесткейса. Для этого задействуем пока что оператор return. И в завершении разбора первого шага следует обратить внимание на использование условного оператора if. Его запись аналогична записи в языке С за некоторыми исключениями. Во-первых, условие может быть помещено в скобки, а может и нет, то есть в нашем примере записи
if( !wChild.Exists() ) и
if !wChild.Exists() идентичны, то есть помещение условного выражения в скобки необязательно. Также в 4Test-е есть отдельный оператор else if, особенностью которого является то, что он находится на одном уровне вложенности, что и верхний if, то есть уменьшается вложенность кода. Соответственно, если у нас есть некоторая громоздкая запись вида:

Code

[ ]if( condition1 )
	[ ] // Some code
[ ]else
	[ ]if( condition2 )
		[ ] // Some another code
	[ ]else
		[ ]if( condition3 )
			[ ]//Some final code
		[ ]else
			[ ]//Some default code

 

идентична записи вида:

Code

[ ]if( condition1 )
	[ ] // Some code
[ ]else if( condition2 )
	[ ] // Some another code
[ ]else if( condition3 )
	[ ]//Some final code
[ ]else
	[ ]//Some default code

 

В данном случае код остается таким же громоздким (кто знает, может так нужно даже), но при этом вложенность остается постоянной и не увеличивается с ростом количества ветвлений. Этот момент желательно учитывать, так как эти варианты кода пишутся одинаково легко, а вот второй вариант читается полегче. Не забывайте, что написанный код надо потом поддерживать, корректировать и т.д.

 

Перейдем ко второму шагу. Он повторяет предыдущий, но тут появляется один нюанс. Мы ждем появления еще одного окна, которое подходит по описанию к wChild. И если мы напишем код второго шага в виде:

Code

	[ ]wTestApp.File.New.Pick()
	[+]if( !wChild.Exists() )
		[ ] LogError("No MDI Child Window #2 appears")
		[ ] return
	[+]else
		[ ]Print(" MDI Child Window #2 appears")

 

то есть по аналогии с предыдущим шагом, то мы допустим несколько неточностей. Во-первых, запись окна wChild с тегом “MDI Child Window*[1]” соответствует активному в данный момент дочернему окну тестируемого приложения, а это может быть любое окно, не обязательно с заголовком “MDI Child Window #2″. Во-вторых, данный код не отловит ошибку, если второе дочернее окно не появилось, так как он среагирует на первое окно, а это не то, что нам нужно. Конечно ничто не мешает нам описать еще одно окно, но уже с тегом “MDI Child Window #2″, но это же многодокументное приложение и нам придется описывать тогда 5, 10 окон, если таковые понадобятся . Лучше как-то обратиться к конкретному окну, зная какой вид может иметь его тег. В таких случаях удобно обращаться к объекту через его оконный класс, а именно:

Code

	[+]if( !wTestApp.ChildWin("MDI Child Window #2").Exists() )

 

Иными словами ищется некоторое окно класса ChildWin с тегом “MDI Child Window #2″, которое является дочерним для окна wTestApp. То есть то, что нам нужно. Таким образом второй шаг полностью записывается в виде:

Code

	[ ]Print("Step 2: Select File > New again")
	[ ]wTestApp.File.New.Pick()
	[+]if( !wTestApp.ChildWin("MDI Child Window #2").Exists() )
		[ ] LogError("No MDI Child Window #2 appears")
		[ ] return
	[+]else
		[ ]Print(" MDI Child Window #2 appears")

 

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

 

Идем далее. В третьем шаге мы выбираем меню File < Close и проверяем, что окно с заголовком “MDI Child Window #2″ закрыто. Тут ничего нового нет, все проверки и действия, необходимые для выполнения данного шага уже были указаны, поэтому реализуем третий шаг следющим кодом:

Code

	[ ]Print("Step 3: Select File > Close")
	[ ]wTestApp.File.Close.Pick()
	[+]if( wTestApp.ChildWin("MDI Child Window #2").Exists() )
		[ ] LogError("MDI Child Window #2 wasn't closed")
		[ ] return
	[+]else
		[ ]Print(" MDI Child Window #2 was closed")

 

По аналогии выполняется и 4-й шаг, в котором выбирается пункт File < Exit, после чего приложение должно закрыться. Так и запишем:

Code

	[ ]Print("Step 4: Select File > Exit")
	[ ]wTestApp.File.Exit.Pick()
	[+]if( wTestApp.Exists() )
		[ ] LogError("Application wasn't closed")
		[ ] return
	[+]else
		[ ]Print("Application was closed")

 

На пятом шаге нужно запустить приложение снова. А давайте запишем это вот так:

Code

	[ ]Print("Step 5: Start Application")
              [ ] apsTestApp( )
	[ ]Print("Main Application window is opened")

 

О том, что мы сделали в этом шаге, будет расписано в следующем пункте. На данный момент достаточно указать, что этим кодом мы запустим наше приложение.

 

А теперь достаточно интересный шаг. Нам нужно перебрать все подпункты меню Controls, дождаться появления нужного диалога, в котором нажать на кнопку Exit. Конечно, можно это все сделать напрямую, вот так:

Code

	[ ] wTestApp.Control.CheckBox.Pick()
	[-] if( !dCheckBox.Exists() )
		[ ] LogError("No check box dialog appears")
		[ ] return
	[ ] dCheckBox.btnExit.Click()
	[-] if( dCheckBox.Exists() )
		[ ] LogError("Checkbox dialog wasn't closed")
		[ ] return
	[ ] 
	[ ] wTestApp.Control.ComboBox.Pick()
	[-] if( !dComboBox.Exists() )
		[ ] LogError("No combo box dialog appears")
		[ ] return
	[ ] dComboBox.btnExit.Click()
	[-] if( dComboBox.Exists() )
		[ ] LogError("ComboBox dialog wasn't closed")
		[ ] return
	[ ] // И так для всех пунктов
	[ ] // ...

 

А что? Работает ведь! Но при этом код во-первых чересчур громоздкий, а во-вторых напрямую дублируются операции. То есть тут полный Копи/Паст. Кстати, бывает, что на этом и останавливаются, мотивируя для себя это тем, что времени на причесывание кода очень мало, лучше вначале написать лишь бы работало, а потом исправлять по мере необходимости. Естественно, есть ряд случаев, когда нужно использовать некоторое быстрое решение, которое само по себе далеко от совершенства, но это самое усовершенствование отложено на неопределенное будущее, когда данную проблему уже придется решать и решать основательно.

 

Даже пример могу привести. Тестировалось GUI приложение и в нем нашлось некоторое окно с текстом. Но сам текст напрямую не брался, так как окно было не стандартным, а CustomWin. Очевидно, нужно было попытаться задействовать какое-то расширение. Мы уже с расширениями работали ( но только с ActiveX ), но с ними была одна большая головная боль – каждое уважающее себя расширение считало своим долгом упасть в самый неподходящий момент. То есть искать еще одно, которое будет еще больше портить картину, не имело смысла. А текст извлечь надо было. А обходное решение было нажать комбинацию клавиш Ctrl-A Ctrl-C (то есть выделить все и скопировать в буфер), а потом из буфера извлечь текст. К счастью, работу с Clipboard-ом SilkTest поддерживает. А на самом деле это было окно BrowserChild, а в нем обычный текст. Уже со временем мы подсоединили IE DOM расширение и реализовали функциональность по извлечению текста напрямую из окна (то есть Clipboard уже не был задействован). Да и сделали так, чтобы эти расширения падали не так уж и часто. Но достаточно длительное время использовалось именно лобовое решение.

 

Но это специфический случай, когда просто нехватало знаний настроек СилкТеста. И все равно надо искать решение. Но как правило, если вы думаете, что экономите время сейчас, решая некоторую проблему влобовую, то скорее всего вы ошибаетесь, так как даже при незначительной модификации продукта ( а при регрессионном тестировании это наверняка произойдет) некоторые действия могут быть недоступными. В нашем примере такой модификацией может стать добавление некоторого пункта меню с соответствующим диалогом. Конечно, можно еще скопировать строчки, но все-таки хотелось бы какого-то более гибкого решения. И все равно придется копаться в этом месиве кода и устанавливать его в нужное место. Вот на этом вы и потратите время, которое вы так старательно экономили ранее. Данный пример может и не очень ярко иллюстрирует проблему, но все-таки найдем более гибкую реализацию 6-го шага нашего тесткейса. Все, кто знаком с программированием, знают, что каждый язык программирования содержит конструкции, которые позволяют повторять некоторые операции циклически. Циклы, то есть. В данном случае очевидно, что в цикле варьируются пункты меню и диалоговые окна, которые должны появиться, а действия над ними идентичны. Соответственно, нужны 2 переменные, которые будут содержать объекты пунктов меню и диалоговых окон. Ну и наверное счетчик. Применив это все, мы можем получить код вида:

Code

	[-] LIST OF WINDOW lwMenus = {...}
		[ ] wTestApp.Control.CheckBox
		[ ] wTestApp.Control.ComboBox
                            .................
	[-] LIST OF WINDOW lwDlgs = {...}
		[ ] dCheckBox
		[ ] dComboBox
                            ..................
	[ ] INTEGER i
	[ ] 
	[-] for ( i = 1 ; i <= ListCount(lwMenus) ; i++ )
		[ ] lwMenus[i].Pick()
		[-] if( !lwDlgs[i].Exists() )
			[ ] LogError("Dialog ""{lwDlgs[i]}"" wasn't opened")
			[ ] return
		[ ] lwDlgs[i].btnExit.Click()
		[-] if( lwDlgs[i].Exists() )
			[ ] LogError("Dialog ""{lwDlgs[i]}"" wasn't closed")
			[ ] return

 

Вот, уже ресурсы от механизмов отделены. Остается только заполнять списки нужными данными. Да, следует обратить внимание, что списки нумеруются с 1. В данном примере конструкция цикла:

Code

	[-] for ( i = 1 ; i <= ListCount(lwMenus) ; i++ )

 

может быть исправлена на более компактную

Code

	[-] for i = 1  to ListCount(lwMenus)

 

Результат будет тот же самый. Последняя конструкция применяется для циклов, у которых счетчик увеличивается на единицу при каждой итерации. Но и это еще не все. Список lwMenus содержит пункты меню, у которых есть вполне определенный родитель, а именно меню Controls, у которого можно извлечь список дочерних элементов, то есть инициализацию данного списка можно упростить и привести к виду:

Code

	[-] LIST OF WINDOW lwMenus = WindowChildren(wTestApp.Controls)

 

Итого получаем код вида:

Code

	[-] LIST OF WINDOW lwMenus = WindowChildren(wTestApp.Controls)
	[-] LIST OF WINDOW lwDlgs = {...}
		[ ] dCheckBox
		[ ] dComboBox
                            ..................
	[ ] INTEGER i
	[ ] 
	[-] for   i = 1  to  ListCount(lwMenus)
		[ ] lwMenus[i].Pick()
		[-] if( !lwDlgs[i].Exists() )
			[ ] LogError("Dialog ""{lwDlgs[i]}"" wasn't opened")
			[ ] return
		[ ] lwDlgs[i].btnExit.Click()
		[-] if( lwDlgs[i].Exists() )
			[ ] LogError("Dialog ""{lwDlgs[i]}"" wasn't closed")
			[ ] return

 

Уже кода меньше, но появляется недостаток. Количество элементов в списке пунктов меню может не совпасть с размером списка диалогов, при этом изначально проконтролировать размерность списков меню нельзя. Тогда ищем кое-какие соответствия. Вот, например, каждому пункту меню соответствует диалог с таким же заголовком, что и пункт меню. А первые слова заголовков окон и текста соответствующих пунктов меню вообще совпадают. Это характеризуется спецификой приложения и вряд ли это правило будет изменено. А теперь оцениваем свои возможности. Мы можем извлечь список пунктов меню, извлечь текст каждого из этих пунктов, а затем искать окно, исходя из его оконного класса и предполагаемого тега, который совпадает с заголовком окна, который формируется в соответствии с выбранным пунктом меню. Круг замкнулся. Все необходимые данные мы можем извлечь. Но тут может быть проблема в несоответствии названий меню и заголовков окон. Но давайте рассмотрим фрейм. Вот необходимое нам меню:

Code

	[+] Menu Control
		[ ] tag "Control"
		[+] MenuItem CheckBox
			..........
		[+] MenuItem ComboBox
			..........
		[+] MenuItem ListBox
			..........
			..........
		[+] MenuItem PageList
			..........
		[+] MenuItem StatusBar
			..........
		[+] MenuItem ToolBar
			..........
		[+] MenuItem TrackBar
			..........
		[+] MenuItem TreeView
			..........
		[+] MenuItem UpDown
			..........

 

А вот так объявлены диалоговые окна:

Code

	[+] window TestApp_Controls dCheckBox
		..........
	[+] window TestApp_Controls dComboBox
		..........
	[+] window TestApp_Controls dListBox
		..........
		..........
	[+] window TestApp_Controls dPageList
		..........
	[+] window DialogBox dStatusBar
		..........
	[+] window DialogBox dToolBar
		..........
	[+] window TestApp_Controls dTrackBar
		..........
	[+] window TestApp_Controls dTreeView
		..........
	[+] window TestApp_Controls dUpDown
		..........

 

В общем, названия меню отличаются от названий диалогов только префиксом d, характерным для диалоговых окон. То есть, если бы можно было как-то манипулировать именами объектов и хранить их в переменных, то можно было бы задать всего один список и всего одну переменную цикла, к которой это все можно перебрать. А такая возможность ведь есть. Вот такой код:

Code

	[ ] // TODO: Add variables declaration here
	[ ] LIST OF STRING lsValue
	[ ] STRING sValue
	...........................
	[+] Print("Step 6: For each menuitem of menu Controls perform the following operations: {chr(10)}"+
          "        - Select Item{chr(10)}"+
          "        - Click on Exit button")
		[+] lsValue = {...}
			[ ] "CheckBox"
			[ ] "ComboBox"
			[ ] "Cursors"
			[ ] "DrawingArea"
			[ ] "KeyboardEvents"
			[ ] "ListBox"
			[ ] "ListView"
			[ ] "PageList"
			[ ] "PopupList"
			[ ] "PushButton"
			[ ] "RadioButton"
			[ ] "ScrollBar"
			[ ] "StaticText"
			[ ] "StatusBar"
			[ ] "TextField"
			[ ] "ToolBar"
			[ ] "TrackBar"
			[ ] "TreeView"
			[ ] "UpDown"
		[ ] 
		[+] for  each sValue in lsValue
			[ ] wTestApp.Control.@(sValue).Pick()
			[ ] 
			[+] if( !@("d{sValue}").Exists() )
				[ ] LogError("Dialog ""{sValue}"" wasn't opened")
				[ ] return
			[ ] @("d{sValue}").btnExit.Click()
			[+] if( @("d{sValue}").Exists() )
				[ ] LogError("Dialog ""{sValue}"" wasn't closed")
				[ ] return
		[ ] 
	[ ] Print("- Appropriate dialog appears{chr(10)}"+
          "- Dialog is closed")

 

выполнит требуемые действия. Естественно, объявления переменных (первые 2 строчки примера) поместите в соответствующее место в начале тесткейса. Следует обратить внимание на оператор @. Строковое выражение, которое следует за этим оператором, воспринимается как имя элемента окна или самого окна. То есть, запись wTestApp.Control.@(sValue).Pick() в зависимости от значения sValue выполняет те же действия, что и wTestApp.Control.CheckBox.Pick(), wTestApp.Control.TextField.Pick() и т.д., когда в sValue содержатся соответственно значения “CheckBox” , “TextField” и т.д. Опять же всплывают такие недостатки, как:

  • Имена пунктов меню должны прямо и четко по одному правилу соответствовать именам диалоговых окон;
  • Список элементов нужно постоянно корректировать при изменении фрейма;

Первый недостаток перестает быть таковым, если выработать четкие правила именования элементов. То есть, что нам мешает в данном конкретном случае обзывать пункты меню и диалоги, соответствующие им, схожими именами? А ничего. Мы проектируем фрейм, с которым нам проще работать, правила формирования которого нам известны и этих правил вполне можно придерживаться.
Второй недостаток не является таким тяжелым как первый, да и сильных модификаций, как правило, для меню не происходит. Более того, можно предыдущий пример реализовать так, чтобы не использовать явно заданные значения, но тем не менее опираться на правила формирования имен меню и соответствующих диалогов. Таким образом можно устранить 2-й недостаток следующим кодом:

Code

	[ ] // TODO: Add variables declaration here
	[ ] WINDOW wCtrl
	[ ] STRING sValue
	...........................

	[-] Print("Step 6: For each menuitem of menu Controls perform the following operations: {chr(10)}"+
          "        - Select Item{chr(10)}"+
          "        - Click on Exit button")
		[ ] 
		[-] for each wCtrl in WindowChildren(wTestApp.Control)
			[ ] sValue = StrTran("{wCtrl}","wTestApp.Control.","")
			[ ] 
			[ ] wCtrl.Pick()
			[ ] 
			[+] if( !@("d{sValue}").Exists() )
				[ ] LogError("Dialog ""{sValue}"" wasn't opened")
				[ ] return
			[ ] @("d{sValue}").btnExit.Click()
			[+] if( @("d{sValue}").Exists() )
				[ ] LogError("Dialog ""{sValue}"" wasn't closed")
				[ ] return
		[ ] 
	[ ] Print("- Appropriate dialog appears{chr(10)}"+
          "- Dialog is closed")

 

То есть, как видно из примера, списки с заранее и в явном виде заданными значениями здесть отсутствуют. Достаточно использовать только переменную sValue и переменную wCtrl.

 

Вот какие возможны варианты реализации данного шага. Любой из приведенных вариантов является правильным (по-своему) и выбор способа реализации подобных задач отводится на усмотрение разработчика. Часть из приведенных реализаций основана на различных допущениях, но каждый разработчик вправе делать эти допущения и приспосабливать свой код под эти допущения. Это не всегда хорошо, особенно, если кто-то еще вздумает корежить ваш код, но все-таки такой подход может позволить решать проблемы не только эффективно, но и красиво.

 

Итак, можно подвести итог по этому пункту. Во-первых, на примере автоматизации некоторого сценария была показана работа со стандартными методами объектов ( все методы для всех стандартных оконных классов можно найти в Help-е). Во-вторых, было показано, что к любому окну можно добираться не только через его объект, описанный во фрейме, но и через его оконный класс (см. шаг 2 ). В третьих, частично показана структура стандартных синтаксических конструкций, таких как условный оператор if и разновидности цикла for ( по мере необходимости будут высвечиваться конструкции и других операторов, но детально останавливаться на них не будем, так как это все описано в Help-e достаточно подробно). А также было показано, как динамически обращаться к элементам окна, сохраняя названия этих элементов в некоторой переменной ( оператор @). Да и просто последовательно решены задачи, которые при работе попадаются достаточно часто, рассмотрены различные варианты и предложены различные решения, а также показаны слабые и сильные стороны тех или иных решений. Безусловно, каждый тестируемый продукт имеет свою специфику и задачи при тестировании различаются в зависимости от продукта. Но в данном пункте мне хотелось отразить ход мыслей, идей, в соответствии с которыми можно принимать те или иные решения; что принимать в расчет и т.п.


Назад Содержание Дальше