Синхронизация в автотестах. Часть 1

Часть 2

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

Как результат, нужно обеспечить темп работы теста таким образом, чтобы он соответствовал темпу работы тестируемого приложения. Как это сделать?

Наиболее простым вариантом является установка задержек. Практически во всех средствах для автоматизации функционального тестирования есть инструкции, которые просто делают паузу в выполнении. Так, во многих решениях присутствуют функции вида: sleep( nSec ) или wait( nSec ). В TestComplete это делается вызовом BuiltIn.Delay(), в Java для этого есть вызов Process.sleep( ). Все эти решения имеют схожую структуру - единственный параметр, указывающий время, на которое установить паузу.

Преимущества данного решения:

  1. Простота - использование встроенной функции/метода достаточно простое и понятное
  2. В некоторых случаях подобные вставки позволяют избежать "заклинивания" выполнения, когда 2 достаточно ресурсоемкие операции выполняются друг за другом ( в частности в SilkTest в некоторых случаях пауза в 1 секунду позволяла избегать заклинивания выполнения команд Агента ).
  3. Достаточно эффективное решение для операций, которые имеют фиксированное время "опоздания", например, появление окна сообщения, выданного клиентским скриптом на веб-странице. В этом случае пауза нужна просто для того, чтобы четко синхронизировать тест с моментом точного появления окна

Тем не менее, у данного решения есть и недостатки:

  1. Нерациональное использование времени выполнения - некоторые операции, особенно, обработка различных клиент-серверных запросов, могут занимать различное время, даже для одной и той же команды. Соответственно, паузу целесообразно делать на интервал времени, покрывающий максимальное время ожидания. В результате, если действие выполнилось раньше, то пауза все равно действует фиксированное время, отчего мы теряем до нескольких минут на одном подобном ожидании. Если подобных действий будет много, то суммарная потеря времени выполнения будет составлять вплоть до нескольких часов.
  2. На разных средах тестируемое приложение может работать с разной скоростью, соответственно, нужно во всех вхождениях инструкции для паузы перестраивать время ожидания.
  3. Подобные паузы не отражают причин, по которым они проставлены, из-за чего подобный код весьма затруднительно читать. Также в тестовом коде может быть слишком много подобных инструкций, отчего код разрастается весьма ощутимо без видимых на то причин
  4. Подобные паузы не гарантируют, что тестируемое приложение достигло нужного состояния

Еще одним решением является возможность притормозить выполнение автоматического теста. Одним из преимуществ автоматического выполнения тестов является скорость выполнения инструкций, но зачастую это становится и главным недостатком, так как инструкции начинают выполняться настолько быстро, что встроенные обработчики тестируемого приложения не успевают среагировать, отчего появляется много ошибок, которые вручную не воспроизводятся в принципе. Во многих более-менее развитых системах для автоматизированного функционального тестирования имеются настройки, позволяющие замедлить операции ввода текста и действий мыши. Зачастую это делается путем установки соответствующих настроек. В некоторых решениях есть специальные инструкции, регулирующие скорость выполнения тестов. Например, в библиотеке Watir есть такое поле как Watir:IE:speed , которое принимает значения :slow или :fast, в зависимости от того, какую скорость выполнения мы хотим задать. Selenium RC имеет аналог - метод setSpeed основного клиентского класса. Какие у данного подхода есть преимущества:

  1. Простота - в данном случае достаточно в одном месте настроить скорость выполнения
  2. Возможность подстроить тесты под реальный темп работы, который обеспечивает пользователь
  3. Оптимальный объем програмного кода тестов - нет необходимости повсеместной установки пауз, в коде достаточно проставить только выполняемые действия

Часть недостатков предыдущего способа (простановка пауз) устраняется при этом подходе, но тем не менее часть из них по-прежнему актуальна:

  1. Нерациональное использование времени выполнения - значительная часть задержек при данном подходе могут оказаться нецелесообразными, а в результате мы можем еще и нивелировать такое преимущество автоматизации тестирования как высокая скорость выполнения. Например, нам нужно заполнить несколько полей, а затем нажать на кнопку. Заполнение полей было бы удобно выполнить в нормальном темпе, когда они заполняются быстро, а не делать паузу в секунду и более. На этом мы теряем много времени
  2. Практически не стабилизирует работу при выполнении запросов, занимающих достаточно произвольное время выполнения. Фактически, данный подход абсолютно не решает проблему синхронизации операций с достаточно широким интервалом времени выполнения.
  3. Нет никакой гарантии, что тестируемое приложение достигло нужного состояния

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

Оба вышеперечисленных подхода не решают одну проблему - контроль над состоянием приложения. А ведь это очень важный момент, так как если приложение вышло из состояния, на которое расчитан тест, то фактически выполнение теста теряет свой смысл и его надо бы либо прекратить, либо восстановить нужное состояние. Но для этого вначале нужно диагностировать, что тестируемое приложение начало вести себя неестественно. Для решения данной проблемы существуют различные методы, которые позволяют опросить тестируемое приложение. Например, все средства автоматизированного функционального тестирования на уровне GUI имеют в своем арсенале функции/методы, проверяющие существование того или иного объекта. Это, как правило, методы вида Exists SilkTest, RFT, Waitr - метод, в TestComplete - это свойство ), win_exists, obj_exists (WinRunner) или что-то типа isElementPresent, как это реализовано в SeleniumRC. Подобные решения проверяют наличие некоторого элемента. Соответственно синхронизация в данном случае реализует следующий алгоритм:

{syntaxhighlighter brush: java;fontsize: 100; first-line: 1; }bObjFound = false;

while( !timeOutExeeded ) {
if( elementPresent( element ) ) {

          bObjFound = true;
          break;
    }

    verifyTimeOut();      

}

if( !bObjFound ) {
Error( “No object available” );
}{/syntaxhighlighter}

Вышеприведенный псевдо-код работает по принципу зацикливания выполнения теста до тех пор, пока не появится нужный элемент либо пока не выйдет максимальное время ожидания. Максимальное время ожидания - это величина, которая устанавливается в зависимости от скорости выполнения приложения. Зачастую, это оценочное время выполнения типичного запроса при нормальной работе системы. Подобный шаблон изначально встроен для проверки на загрузку страниц. В частности у Watir есть такой метод, как Watir::IE.wait . В SeleniumRC присутствуют методы waitForFrameToLoadwaitForPageToLoad, waitForPopup и т.п. 

Поскольку эти решения позволяют контролировать наличие некоторых объектов, то это во многом позволяет контролировать изменение состояния приложения, так как во многом состояние приложения определяется наличием или отсутствием того или иного окна или элемента. Но есть более обобщенный случай, по которому изменение состояния приложения выражается в изменении состояния некоторого объекта. То есть, нам нужно дожидаться, когда некоторый объект примет нужное состояние. В TestComplete имеется метод WaitProperty, который наиболее ярко отражает интерфейс для подобного решения. Этот метод имеет вид:

{syntaxhighlighter brush: java;fontsize: 100; first-line: 1; }obj.WaitProperty( propertyName , propertyValue , timeOut ){/syntaxhighlighter}

Данный метод возвращает True, если свойство, имя которого задано в propertyName принимает значение, заданное в propertyValue до того, как выйдет время ожидания timeOut. Иначе возвращается False. Подобное решение может быть реализовано и в других средствах автотестирования (причем, полезно иметь в арсенале аналоги ). В этом случае синхронизация сводится к проверке перехода системы в нужное состояние ( а точнее переход некоторого контрольного элемента в нужное состояние ).

Безусловно, от этого вырастет объем кода, но при этом:

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

Это были наиболее типичные методы решения проблемы синхронизации. Использование наиболее эффективных решений позволит вдальнейшем упростить разработку и поддержку тестов, особенно если учесть тот факт, что на этапе разработки порядка 60% ошибок автотестов приходятся на ошибки синхронизации. Поэтому правильный подход к решению проблем синхронизации на ранних стадиях автоматизации тестировнаия позволит значительно сократить затраты на поддержку тестов.

Часть 2

Вот один из примеров реализации ожидания объектов для QTP:

{syntaxhighlighter brush: vb;fontsize: 100; first-line: 1; }'Wait for object and returns true or false whether trade is found or not Function WaitForObject(obj, refresh_sec) Dim blnDone, counter 'wait for press button blnDone=obj.Exist counter=1 While Not blnDone Wait (refresh_sec) blnDone=obj.Exist counter=counter+1 If counter=10 then blnDone=True End if Wend WaitForObject = blnDone End Function{/syntaxhighlighter}

Да, все это конечно очень хорошо, но в реализации данного примера есть еще несколько недостатков: 

01 bObjFound = false;
02   
03 while( !timeOutExeeded ) {
04         if( elementPresent( element ) ) {
05   
06               bObjFound = true;
07               break;
08         }
09   
10         verifyTimeOut();     
11 }
12   
13 if( !bObjFound ) {
14         Error( "No object available" );
15 }
1. Эта реализация будет отрабатывать неправильно, в том случае, если на текущей странице будет несколько элементов с одинаковым идентификатором - element !!!
2. Эта реализация не будет работать вообще, в том случае, если идентификатор ожидаемого элемента будет генерироваться динамически !!!
А как показывает практика, такие ситуации встречаются неоднократно.
P.S. Это касается также и функции WaitForObject Laughing
 

 

1. Эта реализация будет отрабатывать неправильно, в том случае, если на текущей странице будет несколько элементов с одинаковым идентификатором - element !!!
2. Эта реализация не будет работать вообще, в том случае, если идентификатор ожидаемого элемента будет генерироваться динамически !!!
А как показывает практика, такие ситуации встречаются неоднократно.
P.S. Это касается также и функции WaitForObject

1. Касательно 1го пункта. На себя эту функцию может взять elementPresent метод. Атомарной целью является ожидание объекта/елемента, а не их количество. Хотя согласен, дополнительный обработчик уникальности инденитификатора потребуется. Спасибо.

2. Этот пункт также связан с первым, идентификация объекта и ожидание объекта - это разные вещи. Почему вы думаете, что именно тут должна происходит идентификация?

Start-Process 'some too slow process' -PassThru | Get-UIAWindow -Seconds 300 | Get-UIAButton -n Next* -Seconds 120 | Wait-UIAButtonIsEnabled -Seconds 60 | Invoke-UIAButtonClick;

Это пример относительно большого сетапа, который оформлен в виде сьюты (экзешник запускает по очереди несколько msi'ев, который могут быть как в общем визарде, так и в своих).

Сначала окошко имеет один вид, потом другой. Поэтому установлено максимальное время ожидания в 5 минут. Потом ещё может быть задержка с появлением кнопки Next (сетапный визард ищет разнообразные фрейворки), потом ещё Next становится доступной (инейбленной) только через промежуток времени (когда кликать ещё нельзя).

Максимальное время этого кода - 300+120+60 секунд, не считая нескольких секунд на поиск окна (оно там ведь не одно, сначала одно, потом другое), контрола и его инейбленности.

На быстрой машине код выполнится, разумеется, гораздо быстрее.

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

 

Кстати, есть и другой способ ускорения тестов: если окно не наййдно, то чтобы поиск контролов не производился на несуществующем окне с дефолтными таймаутов в 5 секунд для каждого контрола :), сделано так:

дефолтный таймайут резко уменьшается

контролы всё равно ищутся на несуществущем окне, но ожидания их появления почти нет

тест проходит неудачный участок в несколько раз быстрее.

К нормлаьно работе тест возвращается тогда, когда он словит какое-нибудь окно.

 

Никакие do/while и прочие слипы пользователь писать не обязан! Пользователь только пишет/генерит актуальный код, добавляя параметры -Timeout или -Seconds в случае острой необходимости.