2. Техники и подходы в автоматизации

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

Мы приступаем ко второй главе, в которой остановимся на практических аспектах автоматизации функционального тестирования. Если до этого мы говорили о том, что нужно делать, то теперь поговорим о том, как это можно сделать.

Для примеров мы выбрали инструмент компании SmartBear TestComplete, так как он является наиболее популярным инструментом в русскоязычной среде. TestComplete позволяет использовать несколько языков для создания скриптов, среди них – JScript, который мы и будем использовать в наших примерах[1].

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

В TestComplete все объекты системы (будь то процессы, окна или элементы управления) представлены в виде дерева, которое можно увидеть на специальной панели Object Browser (рис. 2.1).


Рис. 2.1. Object Browser в TestComplete

Главным объектом системы является объект Sys, используя его свойства можно, например, получить доступ к буферу обмена (свойство Clipboard) или другим элементам системы.

Дочерними элементами объекта Sys являются процессы (например, TestComplete или WINWORD). У процессов также есть различные свойства (например, Path, который содержит полный путь к запускаемому файлу) и другие дочерние объекты – окна (Window). Некоторые окна являются невидимыми и нам никогда не придется с ними работать, но тем не менее в системе они существуют и TestComplete их отображает в дереве объектов. Соответственно, у каждого окна есть свои дочерние элементы – это, собственно, элементы управления, с которыми работает пользователь (кнопки, списки, текстовые поля и т.д.). С точки зрения системы они также являются «окнами», потому для доступа к ним используется метод Window(как и для окон).

У каждого объекта (будь то объект Sys, процесс или элемент управления) также есть методы (Methods), которые мы можем использовать для этих объектов, например: Click() – щелкнуть левой кнопкой мыши по объекту, Refresh() – обновить информацию об элементе, Keys() – ввести в элемент какой-то текст.

В качестве примера приведем код для нажатия на кнопку «плюс» в окне Калькулятора:

Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus").Window("Button", "+").Click()

В этом примере:

  • Sys – главный объект – система
  • Process(“CalcPlus”) – обращение к процессу Калькулятора по имени (CalcPlus)
  • Window(“SciCalc”, “Calculator Plus”) – обращение к главному окну Калькулятора (SciCalc – класс окна, Calculator Plus – его заголовок
  • Window(“Button”, “+”) – обращение к кнопке «плюс» (Button – класс окна, в нашем случае это кнопка, + – текст на кнопке)
  • Click() – обращение к методу, который осуществляет клик левой кнопкой мыши

Приведем еще несколько примеров работы с объектами в TestComplete.

Sys.Process("CalcPlus").Exists – проверка существования процесса
Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus", 1).Close() – закрыть окно Калькулятора
Sys.Process("CalcPlus").Terminate() – уничтожить процесс (аналогично выполнению команды Завершить процесс в окне Диспетчер задач).

Теперь, когда вы имеете представление о том, как TestComplete работает с объектами, можно приступить к изучению различных подходов к автоматизации[2].

2.1 Запись и воспроизведение (Record & Playback)

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

Создавать рабочие скрипты таким образом не рекомендуется по нескольким причинам:

  1. Генерация неоптимального кода. Средства записи не умеют генерировать условия и циклы, без которых можно создать лишь простейшие скрипты.
  2. Переменные, которые генерируются в результате записи, зачастую имеют непонятные или громоздкие имена, что усложняет поддержку кода в будущем.
  3. Код скриптов, написанный вручную, обычно лучше структурирован и более читабельный.
  4. Средства записи позволяют записать лишь действия пользователя с приложением, однако зачастую в скриптах приходится выполнять и другие действия (например, вычисления), которые записать невозможно.
  5. Если в приложении используются нестандартные элементы управления (например, ActiveX), средство записи не сможет отследить обращение к их внутренним свойствам и методам. В таких случаях обычно записываются «поверхностные» действия (такие как щелчки мыши по определенным координатам или ввод текста). В результате малейшее изменение в тестируемом приложении (например, изменение размера элемента управления) повлияет на работоспособность скриптов.

Есть, однако, несколько случаев, в которых применение записи оправдано:

  1. Изучение инструмента. Во время изучения инструмента запись – самый простой и быстрый способ изучения инструмента: как работать с окнами, элементами управления и т.п.
  2. Запись скрипта с целью его дальнейшей модификации вручную. Если запись в инструменте автоматизации достаточно продвинутая, в некоторых случаях бывает проще и быстрее записать некоторые действия, а затем внести небольшие изменения.
  3. В некоторых инструментах при записи скрипта одновременно формируется «карта» окна (или страницы) приложения (например, класс или другая структура, содержащие все необходимые элементы). Если запись – самый простой способ создания такой структуры, то имеет смысл воспользоваться ею хотя бы один раз, чтобы создать эту структуру и дальше работать с ней, создавая тестовые скрипты вручную.

К достоинствам записи также можно было бы отнести возможность создания автоматических скриптов людьми, которые совершенно незнакомы с программированием, однако в этой книге мы не рассматриваем этот вариант (см. главу 1.1 Команда, или «кто должен заниматься автоматизацией?»).

В TestComplete можно записать 2 вида тестов: скриптовые и keyword-driven. О втором подходе мы поговорим далее в этом разделе, а сейчас рассмотрим создание обычного скрипта.

Предположим, нам необходимо в окне Калькулятора нажать последовательно кнопки 1, 2, 3, 4, 5 и 6. Включим в TestComplete запись (меню Test – Record – Record Script), запустим Калькулятор (например, из приложения FAR Manager), нажмем все необходимые кнопки, остановим запись и посмотрим, что мы получили в итоге:

function Test1()
{
  var  wndConsoleWindowClass;
  var  wndSciCalc;
  wndConsoleWindowClass = Sys.Process("Far").Window("ConsoleWindowClass", "*");
  wndConsoleWindowClass.Keys("[Enter]");
  wndSciCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  wndSciCalc.Window("Button", "1").ClickButton();
  wndSciCalc.Window("Button", "2").ClickButton();
  wndSciCalc.Window("Button", "3").ClickButton();
  wndSciCalc.Window("Button", "4").ClickButton();
  wndSciCalc.Window("Button", "5").ClickButton();
  wndSciCalc.Window("Button", "6").ClickButton();
}

Совершенно очевидно, что это – не самый лучший пример кода, а ведь мы всего лишь запустили приложение и нажали 6 кнопок.

Первое, что бросается в глаза, – это запуск Калькулятора. Во время записи в FAR’е была открыта нужная нам папка, поэтому для запуска приложения нам достаточно было нажать [Enter]. Однако при запуске скриптов даже сам FAR может быть не запущен, не говоря уж о том, какая папка окажется в нем текущей. Совершенно очевидно, что запуск приложения нужно делать иначе. Для этого мы воспользуемся специальным объектом TestComplete, который называется TestedApps.

Далее, нажатие на 6 кнопок подряд лучше было бы организовать в цикле. Тогда в случае, если позже нам придется изменить наш тест, чтобы он нажимал кнопки от 1 до 9, нам придется внести всего одно изменение, вместо того, чтобы копировать последнюю строку 3 раза, а затем в трёх строках менять цифры.

Вот как мог бы выглядеть код, написанный вручную:

function Test2()
{
  TestedApps.CalcPlus.Run();
  var CalcWnd = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  for(var i = 1; i <= 6; i++)
  {
    CalcWnd.Window("Button", i).ClickButton();
  }
}

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

Однако мы можем пойти дальше и написать функцию, которая будет принимать в качестве параметра строку с цифрами (например, “123456”) и нажимать в Калькуляторе соответствующие цифры. Вот пример такой функции:

function EnterNumbers(nums)
{
  var CalcWnd = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  for(var i = 0; i < nums.length; i++)
  {
    CalcWnd.Window("Button", nums.substr(i, 1)).ClickButton();
  }
}

А вот как теперь будет выглядеть сам тест:

function Test3()
{
  TestedApps.CalcPlus.Run();
  EnterNumbers("123456");
}

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

Мы рекомендуем всегда писать тесты вручную и стараться не пользоваться средствами записи, кроме случаев, оговоренных выше.

2.2 Декомпозиция (module-based)

Функциональная декомпозиция – это разделение кода скриптов на отдельные функции/методы, выполняющие определенные действия, которые затем вызываются в тестовых скриптах. Попросту говоря, не стоит вываливать все действия прямо в тест, некоторые действия лучше выделить в отдельные функции. Во-первых, это предотвратит в дальнейшем дублирование кода, во-вторых, такие тесты гораздо удобнее читать и модифицировать.

Например, в отдельные функции можно выделить:

  • Навигация. Любые действия, связанные с навигацией по приложению, скорее всего будут выполняться довольно часто. Даже для простого действия (например, открытие окна поиска) можно написать отдельную функцию. Согласитесь, что строка вида
openSearch()

выглядит лучше, чем просто обращение к пункту меню

Sys.Process("notepad").Window("Notepad", "*").MainMenu.Click("Edit|Find...");

Более того, в функции openSearch() мы можем предусмотреть проверку того, что окно Search действительно открылось.

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

В декомпозицию также входит размещение функций/методов в соответствующих отдельных модулях. Например, можно все высокоуровневые функции проверок поместить в один модуль, а функции заполнения – в другой. Большие приложения состоят как бы из отдельных частей, поэтому имеет смысл выносить в отдельный модуль всё, что связано с конкретной частью функциональности приложения (например, в случае с простым приложением Блокнот мы можем создать отдельные модули File, Edit, Format, View и Help, таким образом разбив наше приложение в соответствии с пунктами меню; другой пример – группировка по функциональности, когда в модуле Search находятся функции для работы с поиском и заменой, в модуле Print – всё, что необходимо для работы с пунктами меню Page Setup и Print, и т.д.).

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

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

В целом в функциональной декомпозиции можно выделить 3 уровня:

  • Фреймворк. Самый «высокоуровневый» код, который может использоваться не только для любых частей приложения, но и даже в нескольких проектах (если инструмент позволяет это делать).
  • Код приложения[3]. На этом уровне хранится код, который используется только для конкретного приложения (проверки, навигация и т.п.).
  • Код тестов. Собственно тесты, которые мы запускаем.

На каждом уровне может существовать несколько дополнительных подуровней. Например, код фреймворка может быть разделен на код работы с логом, код работы с базами данных, код обработки исключений и т.д.; код приложения может делиться на подуровни, как было описано выше в примере с Блокнотом; тесты также могут относиться к разным группам (например, их можно разбить на группы, каждая из которых будет соответствовать определенной части приложения; другой подход – выделить smoke test и тесты для полного тестирования).

Три простых общих правила при работе с функциями на разных уровнях:

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

Иногда у вас может возникать желание нарушить третье правило и спроектировать тесты таким образом, чтобы один из них зависел от другого, а значит их нужно либо запускать в определенной последовательности, либо вызывать один тест из другого. Это плохая практика, каждый тест должен иметь возможность запускаться отдельно от остальных, подготавливая в случае необходимости тестируемое приложение, данные и т.п. Этому есть простое объяснение: если у вас есть 3 теста, которые зависят от четвертого, то в случае, если 4й тест прошел с ошибкой, остальные три также будут содержать ошибки, хотя на самом деле с ними всё в порядке.

Пожалуй, наиболее ярким примером такого подхода является процесс тестирования инсталляции приложения. Кажется, что имеет смысл написать три теста (install, modify, uninstall), которые будут запускаться именно в таком порядке, таким образом «помогая» друг другу. В такой ситуации лучше написать вспомогательную функцию на уровне кода приложения, которая будет проверять, установлено ли приложение, и если нет – то устанавливать его наиболее простым способом (т.е. избегая многочисленных проверок, которые мы бы сделали в обычном тесте, например с помощью silent install). Затем эта функция вызывается в начале каждого теста, которому необходимо установленное приложение (в нашем примере это modify и uninstall). Если приложение уже установлено, то мы просто переходим к следующему шагу теста, если нет – быстро устанавливаем его и дальше продолжаем выполнение теста.

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

Здесь можно сразу выделить несколько необходимых нам методов: ввод числа, нажатие на кнопки «+» и «=», проверка результата и очистка поля. В тесте мы создадим массивы складываемых чисел и ожидаемых результатов, которые затем будем прогонять в цикле. Опять же для простоты мы предполагаем, что на момент запуска теста Калькулятор уже открыт.

Вот как выглядят функции кода приложения.

function enterNumber(num)
{
  var wCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  for(var i = 0; i < num.length; i++)
  {
  wCalc.Window("Button", num.substr(i, 1)).Click();
  }
}

function verifyResult(expectedResult)
{
  var wCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  var actualResult = wCalc.Window("Edit", "").wText.split(",")[0]
  if(expectedResult != actualResult)
  {
    Log.Error("Incorrect result", "Expected: " + expectedResult +
    "\nActual: " + actualResult);
  }
}


function clickPlus()
{
  var wCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  wCalc.Window("Button", "+").Click();
}

function clickEqual()
{
  var wCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  wCalc.Window("Button", "=").Click();
}


function clearResult()
{
  Log.Message("Clearing result pressing [Esc] key");
  var wCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  wCalc.Keys("[Esc]");
}

 

А вот как выглядит тест.

function TestPlus()
{
  var nums1 = ["5", "13", "124"];
  var nums2 = ["42", "3", "24"];
  var results = ["47", "20", "148"];

  var wCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  wCalc.Activate();

  for(var i = 0; i < nums1.length; i++)
  {
    clearResult();
    Log.Message("Adding " + nums1[i] + " and " + nums2[i]);
    enterNumber(nums1[i]);
    clickPlus();
    enterNumber(nums2[i]);
    clickEqual();
    verifyResult(results[i]);
  }
}

Мы специально ввели один неправильный результат (20 вместо 16), чтобы продемонстрировать, как будет выглядеть ошибка в логе.


Рис.2.2. Лог выполненного теста

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

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

Например, в нашем примере мы создали две функции: clickPlus() и clickEqual(). Если бы это был реальный проект, то сразу можно было бы создать аналогичные функции для других действий (например, clickMinus, clickDivide и т.п.), или даже сразу объединить их в одну функцию clickAction() с параметром.

2.3 Тесты, управляемые данными (data-driven)

Data-driven (DDT) – это очень мощный и широко используемый подход в автоматизации тестирования, суть которого заключается в отделении данных от кода, помещая все данные в отдельное хранилище и считывая их по мере надобности. На сегодняшний день практически все инструменты, предназначенные для автоматизации тестирования, поддерживают data-driven подход.

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

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

Вот примеры данных, которые обычно заносятся в таблицы:

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

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

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

Для начала откроем Excel и создадим в нем страничку Plus с нужными данными.


Рис. 2.3. Данные в Excel файле

Первая строка таблицы содержит имена колонок, все остальные строки – данные, соответствующие этим колонкам.

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

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

//USEUNIT Func_Decomposition

function TestCalcDDT()
{
  var excel = DDT.ExcelDriver(Project.Path + "ddt_data.xls", "Plus");

  var wCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  wCalc.Activate();

  while(!excel.EOF())
  {
    clearResult();
    Log.Message("Adding " + excel.Value("num1") + " and " +
    excel.Value("num2"));
    enterNumber(excel.Value("num1").toString());
    clickPlus();
    enterNumber(excel.Value("num2").toString());
    clickEqual();
    verifyResult(excel.Value("result").toString());
    excel.Next();
  }
  DDT.CloseDriver(excel.Name);
}

Как видите, пример из предыдущей главы практически не изменился, однако теперь все данные можно хранить в Excel файле. В дальнейшем, скорее всего, нам бы понадобилось добавлять аналогичные тесты для операций «минус», «умножить» и «разделить». Используя Excel, мы просто добавим новые листы в файл и внесем туда все необходимые данные, а при обращении к драйверу (DDT.ExcelDriver) вторым параметром передадим соответствующее имя.

Обратите внимание на использование метода toString() при считывании данных из файла. Формат данных в ячейках Excel может быть разным и в случае необходимости считанные данные необходимо преобразовывать. Например, в нашем случае в ячейках хранятся целые числа и если не преобразовывать их в строки, функция enterNumber() будет интерпретировать число как пустую строку и числа вводится не будут. Другой способ – всегда хранить в таблицах только строки. При этом вам, возможно, придется выполнять в скриптах обратное преобразование (из строки в другие типы), однако по крайней мере вы будете всегда уверены в том, что при считывании из таблицы получаете строку.

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

function GetRowById(fileName, sheetName, id)
{
  var row = new Object();
  var excel = DDT.ExcelDriver(fileName, sheetName);
  while(!excel.EOF())
  {
    if(excel.Value("id") == id)
    {
      for(var i = 0; i < excel.ColumnCount; i++)
      {
        colName = excel.ColumnName(i);
        row[colName] = excel.Value(colName);
      }
    DDT.CloseDriver(excel.Name);
    return row;
  }
  excel.Next();
}

DDT.CloseDriver(excel.Name);
return null;
}

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

function TestId()
{
  var row = GetRowById(Project.Path + "ddt_data.xls", "Plus", 2);
  Log.Message(row["result"])
}

Этот пример кода выведет нам в лог число 20, которое хранится в строке 2, колонка result.

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

2.4 Тесты, управляемые ключевыми словами (keyword-driven)

Keyword тесты – это подход, позволяющий (по крайней мере теоретически) упростить написание тестов за счет того, что тестировщикам не приходится писать код скриптов, а достаточно скомпоновать тесты из имеющихся ключевых слов (keyword’ов) с параметрами, как при использовании конструктора.

На данный момент существует два основных вида keyword тестов. Они не имеют каких-либо названий, поэтому мы их назовём «простой» и «сложный».

«Простые» keyword тесты

«Простые» keyword тесты можно создавать, например, с помощью встроенных средств инструментов TestComplete и Quick Test Pro. Суть их заключается в том, что для каждого элементарного действия, которое мы можем сделать (щелчок мышью, ввод текса, выбор пункта меню) в конструкторе предусмотрено ключевое слово и набор параметров, которые необходимо им передавать (например, в случае ввода текста параметром служит вводимый текст). Фактически каждое ключевое слово в данном случае – это аналог какого-то метода (например, команда Post Screenshot из набора keyword’ов TestComplete – это аналог метода Log.Picture).

Несмотря на своё предназначение (упрощение создания тестов), у «простых» keyword тестов есть ряд серьёзных недостатков:

  • Создание keyword теста – процесс довольно трудный (если речь идет не об автоматической записи, конечно), так как для каждого действия необходимо делать большое количество кликов мышью (выбрать подходящее действие, ввести кучу параметров и т.п.).
  • Редактирование такого теста – задача также не из простых (по той же причине). Создание циклов, условий и других подобных конструкций в keyword тестах сложнее, чем написание аналогичного кода скрипта.
  • Ограниченное число команд. Не все действия можно реализовать с помощью keyword’ов, поэтому в какой-то момент придется либо всё-таки прибегнуть к программированию, либо отказаться от автоматизации каких-то задач.
  • Даже при использовании интуитивно понятного редактора keyword тестов человек, не имеющий опыта в программировании, вряд ли сможет создать более-менее сложные и надёжные тесты, поэтому без опытного автоматизатора всё равно не обойтись

Чтобы посмотреть, как выглядит keyword тест в TestComplete, достаточно выбрать пункт меню Test – Record – Record Keyword Test, выполнить какие-то действия в тестируемом приложении, после чего остановить запись (аналогично записи обычного скриптового теста). Например, на рисунке 2.4 показан пример теста, который выполняет в Калькуляторе действие «2+3» и проверяет полученный результат.


Рис. 2.4. Keyword тест в TestComplete

Если мы теперь, например, захотим выполнить это действие 3 раза, нам придется добавить в нужном месте элемент For Loop, создать для него переменную, задать начальное и конечное значения, а также при необходимости шаг цикла (Рис. 2.5).


Рис. 2.5. Keyword тест с циклом

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

«Сложные» keyword тесты

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

Например, одно ключевое слово может служить для вызова нескольких функций, каждая из которых принимает разное количество параметров и в результате которой выполняется большое количество действий. Сами тесты при этом могут храниться в базе данных или в data-driven таблицах.

Собственно, «сложные» keyword тесты представляю собой ничто иное как более продвинутый вариант data-driven подхода. При этом часть кода может даже храниться в таблицах и вызываться из скриптов динамически (например, в языке JScript с помощью функции eval можно выполнить любой JavaScript код, переданный в виде строкового параметра), однако отладка таких скриптов сильно затрудняется.

Создание таких тестов состоит из двух частей:

  • Создание собственно тестов с помощью ключевых слов. Тесты обычно хранятся в таблице (Excel, база данных и т.п.).
  • Создание «драйвера» – набора функций или класса, которые будут выполнять определенные действия в зависимости от выбранного ключевого слова.

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

В качестве примера создадим простой тесткейс, который будет вычислять в Калькуляторе сумму двух чисел. Вы сами можете решать, что создавать сначала: тесты или драйвер. Мы решили начать с теста. На рисунке 2.6 показано, как он выглядит в Excel файле:


Рис. 2.6. Пример keyword тесткейса

Имя листа в данном случае (Test1) соответствует имени тесткейса. Соответственно на одном листе у нас будет только один тест.

Первая колонка (Step) – это номер шага. Вторая (Action) – это собственно действие, которое мы хотим выполнить в этом шаге (шаг 1 – открыть приложение; шаг 2 – посчитать; шаг 3 – проверить результат; шаг 4 – очистить результат).

Дальше идут колонки параметров. Так как параметры будут разными в зависимости от действия, колонки названы просто Param1, Param2 и т.д. Например, для действия OPENAPP (открыть приложение) нам нужно передать всего один параметр: имя приложения. Для действия CALCULATE (посчитать) нам нужно передать много параметров (два числа и действие), а также способ вода данных (BY_KEYBOARD означает, что будет эмулироваться ввод данных с клавиатуры, BY_MOUSE – нажатие на кнопки с помощью мыши) и т.д. Если в дальнейшем количество параметров для какого-то действия увеличится, мы просто добавим новую колонку.

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

function DoAction(action)
{
  switch(action)
  {
    case "OPENAPP":
      Log.Message("Running application " + arguments[1]);
      TestedApps.Items(arguments[1]).Run();
      break;
  
    case "CALCULATE":
      Log.Message("Calculating");
      var wCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
      if(arguments[4] == "BY_KEYBOARD")
      {
        for(var i = 1; i <= 3; i++)
        {
          wCalc.Keys(arguments[i]);
        }
        wCalc.Keys("=");
      }
      else  // BY_MOUSE
      {
        for(var i = 1; i <= 3; i++)
        {
          wCalc.Window("Button", arguments[i]).Click();
        }
        wCalc.Window("Button", "=").Click();
      }
      break;

  case "VERIFY":
    Log.Message("Verifying result");
    var wCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
    var result = wCalc.Window("Edit", "").wText.split(",")[0];
    if(result != arguments[1])
    {
      Log.Error("Wrong result", "Exp: " + arguments[1] + "\nAct: " + result);
    }
    break;

  case "CLEAR":
    Log.Message("Clearing result");
    var wCalc = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
    if(arguments[4] == "BY_KEYBOARD")
    {
      wCalc.Keys("[Esc]");
    }
    else  // BY_MOUSE
    {
      wCalc.Window("Button", "C").Click();
    }
    break;

  default:
    Log.Error("Unknown action: " + action);
    break;
  }
}

И теперь нам осталось лишь написать небольшую функцию, которая будет собирать всё это добро вместе и выполнять тест. Вот пример такой функции:

function TestRunner()
{
  var test = DDT.ExcelDriver(Project.Path + "keyword_data.xls", "Test1");
  while(!test.EOF())
  {
    DoAction(test.Value("Action"), test.Value("Param1"), test.Value("Param2"), test.Value("Param3"), test.Value("Param4"));
    test.Next();
  }
  DDT.CloseDriver(test.Name);
}

Это – очень сильно упрощенный пример keyword-driven подхода. Мы не ставили целью создать полноценный фреймворк, а лишь объяснили основной принцип действия тестов, управляемых ключевыми словами. Если вы захотите воспользоваться этим подходом, вам придется написать гораздо больше дополнительного кода, который будет запускать все ваши тесты. Пожалуй, здесь нельзя выделить плохие или хорошие подходы, вы сами должны решать, насколько сложным будет ваш фреймворк.

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

Keyword-driven методология – очень мощный и гибкий подход к написанию тестов, однако он требует хорошего уровня программирования от тех, кто будет создавать код драйвера и код запуска тестов.

Основное преимущество этого подхода – отдельное хранение тестов в удобочитаемом виде (не только для автотестера, но и для любого другого человека). Довольно часто этот подход используется для того, чтобы создавать тесты могли не только технически подкованные специалисты, но и те, кто понятия не имеет о программировании. При этом в команде выделяются две основные роли: те, кто пишут код скриптов, и те, кто создает собственно тесты.

Основной недостаток подхода – сложность создания фреймворка.

2.5 Тесты, управляемые объектами (object-driven/page object)

Тесты, управляемые объектами (Object-driven Testing, ODT) – это методология, при использовании которой тестируемое приложение в тестах представлено в виде класса (или нескольких классов). Для выполнения того или иного действия в приложении необходимо вызывать различные методы этого класса.

Некоторые инструменты автоматизации изначально разработаны под эту методологию (ярким примером такого инструмента является SilkTest, в котором приложение представлено в виде winclass’ов и экземпляров этих классов – window). Другие инструменты могут косвенно поддерживать эту методологию благодаря возможностям языка. В TestComplete есть два способа: использование специального объекта ODT или использование языковых возможностей. Мы воспользуемся вторым подходом для демонстрации ODT.

Конечно, в случае ODT подхода никто не запрещает нам использовать одновременно обычные функции, не являющиеся методами класса. ODT всего лишь помогает представить тестируемое приложение и работу с ним в более удобном виде.

Например, вместо функции openApp(), в которой параметром выступает имя запускаемого приложения, мы будем использовать обращение к методу App.Open(); для проверки существования окна – свойство Exists (App.Exists) и т.д.

Рассмотрим простой пример класса Калькулятора. В JavaScript для объявления класса служит то же ключевое слово, что и для объявления функций. Внутри класса с помощью специального синтаксиса объявляются свойства и методы. В нашем примере нам понадобится 3 метода: start(), calculate() и close(), и одно свойство mainWin, с помощью которого мы будем обращаться к главному окну приложения. Вот объявление этого класса.

function Calculator()
{
  this.mainWin = null;
    this.start = function()
  {
    if(!Sys.WaitProcess("CalcPlus").Exists)
    {
      TestedApps.CalcPlus.Run();
    }
    this.mainWin = Sys.Process("CalcPlus").Window("SciCalc", "Calculator Plus");
  }
  
  this.close = function()
  {
    this.mainWin.Close();
  }
  
  this.calculate = function(expression)
  {
    for(var i = 0; i < expression.length; i++)
    {
      this.mainWin.Keys(expression.substr(i, 1));
    }
    this.mainWin.Keys("=");
    return this.mainWin.Window("Edit", "").wText;
  }
}

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

function TestCalcODT()
{
  // создаем объект класса Калькулятор
  var calc = new Calculator();

  calc.start();
  calc.mainWin.Activate();
  var result = calc.calculate("12+3-8");
  Log.Message(result)
  calc.close();
}

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

Объектами в ODT подходе могут выступать не только тестируемые приложения, но и другие сущности. Например, мы можем создать класс User, который будет представлять собой пользователя, взаимодействующего с тестируемым приложением и т.п. Здесь всё зависит исключительно от ваших потребностей и фантазии.

Также стоит упомянуть, что при тестировании веб-приложений Object-driven testing зачастую называют подходом, основанным на Page Object’ах. При этом каждая страница веб-приложения описывается отдельным классом со своими свойствами и методами, поэтому по сути это одно и то же.

2.6 Тесты, управляемые моделями (model-based)

Тесты, управляемые моделями (Model-based Testing, MBT) – это отдельная наука, стоящая особняком в ряду подходов к автоматизации тестирования. В MBT используются совершенно другие инструменты и техники, на эту тему написано множество статей, и в рамках нашего учебника мы не сможем достаточно полно рассмотреть этот подход, а дадим лишь краткое описание.

Фактически MBT – это наиболее правильный подход к функциональному тестированию, так как он позволяет провести наиболее полное тестирование функциональности. Вместе с тем, MBT подход является наиболее трудоёмким.

Модель – это математическое описание приложения, описывающее его поведение и/или процесс тестирования приложения. В зависимости от сложности модели, мы можем провести более или менее точное тестирование приложения.

Для того, чтобы объяснить сказанное выше, возьмём в качестве примера Калькулятор.


Рис. 2.7. Режимы работы Калькулятора

Как видно на рисунке 2.7, в Калькуляторе есть три режима: Стандартный (Standard), Инженерный (Scientific) и режим Конвертирования (Conversion). У каждого режима доступен свой набор команд и есть свой набор особенностей и ограничений, соответственно для каждого режима нам понадобится своя модель.

Например, в Стандартном режиме у нас есть команда «корень квадратный» (кнопка sqrt), однако перейдя в режим Инженерный мы обнаружим, что такой кнопки нет. Для вычисления квадратного корня нам потребуется возводить число в степень ½.

А вот другой интересный пример с Калькулятором. В детстве был популярен вопрос с подвохом: «сколько будет два плюс два умножить на два?». Первый ответ, приходящий в голову отвечающего, был «8», так как действия в голове выполнялись в том порядке, в каком их называл спрашивающий. И лишь позже мы понимали, что на самом деле правильный ответ – «6», так как операция умножения имеет приоритет перед сложением.

Попробуйте в Калькуляторе выполнить действия «2+2*2=» в Стандартном режиме и в Инженерном. Вы обнаружите, что одна и та же последовательность действий даёт в итоге два разных результата («8» в Стандартном режиме и «6» – в Инженерном). Это происходит потому, что инженерные калькуляторы учитывают приоритет операций, а обычные – нет[4].

Оба этих примера – демонстрация различных моделей поведения одного и того же приложения.

Другие варианты моделей, которые можно придумать для Калькулятора Windows: модель элементарных операций (+, -, *, /), тригонометрическая модель, модель статистических вычислений, модель работы с памятью, модель систем счисления и т.д.

Для каждой модели можно предусмотреть свою точность. Например, в случае с элементарными операциями, мы можем работать либо с целыми числами, либо с целыми и дробными числами; в случае вычислений логарифмов, корней и работы с тригонометрическими функциями, мы можем определить значимое количество знаков после запятой и т.п.

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

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

Выбор точности модели обычно зависит от тестируемого приложения. Например, высокая точность очень важна в финансовых приложениях и жизненно необходима в медицинских, однако ею можно пренебречь при тестировании портала знакомств (конечно, будет немного странно, если в анкете ваш возраст будет указан 26,03 года, однако в конце концов от этого никто не пострадает).

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

Как видно из примеров выше, даже в случае простых приложений (Калькулятор, инсталлятор) у нас появляется огромное количество вариантов, с которыми необходимо как-то работать, так что же говорить о сложных приложениях? Именно поэтому подход Model-based применяется обычно для приложений, к которым предъявляются повышенные требования качества. И именно поэтому для этих целей разработаны специальные инструменты, которые выходят за рамки рассмотрения этой книги.

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

Для создания model-based тестов вам понадобится создать несколько сущностей, которые будут взаимодействовать между собой:

  • Драйвер интерфейса – набор функций, осуществляющих взаимодействие с тестируемым приложением (ввод и получение данных). Например, в Калькуляторе ввод данных может осуществляться как с клавиатуры, так и мышью, причем набор действий может быть весьма многообразным. А вот получение данных, в принципе, может оказаться довольно тривиальным и свестись к получению значения текстового поля.
  • Хранилище состояния приложения – набор параметров, определяющих, в каком состоянии находится в данный момент приложение. Например, система счисления, система измерения углов, находится ли Калькулятор в состоянии ошибки и т.п. В зависимости от текущего состояния приложения, некоторые функции могут быть недоступны.
  • Средства сравнения эталонных значений с полученными. В случае с Калькулятором здесь могут выполняться проверки не только значения из текстового поля, но также доступность или недоступность некоторых элементов управления (например, кнопки A, B, … E не должны быть доступны при любой системе счисления, кроме шестнадцатеричной).
  • Генератор тестовых данных. Набор функций, создающих разнообразные данные для выполнения тестирования.
  • Генератор тестов. Тесты в Model-based подходе являются абстрактными сущностями, т.е. они не завязаны на конкретные данные и/или пути выполнения приложения. Следовательно нам необходимо иметь возможность генерировать конкретные тесты из имеющихся абстрактных описаний.

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

Отметим лишь, что даже если вы не планируете использовать model-based тестирование в своем проекте, вы можете взять на вооружение некоторые из его подходов. Например, средства сравнения эталонных данных с полученными широко используются при тестировании функциональности CRUD (Create/Read/Update/Delete), когда вы один раз создаете в тесте структуру или запись, а затем заполняете поля формы или окна значениями из этой структуры, после чего открываете запись в приложении и сравниваете сохранённые данные с эталонными, хранящимися в созданной в самом начале структуре.

2.7 Синхронизация выполнения тестов

Синхронизация – это, пожалуй, самый важный вопрос в функциональном тестировании, с которым необходимо разобраться независимо от того, какой инструмент вы используете или какой тип приложения тестируете. В зависимости от того, насколько эффективно вы будете использовать синхронизацию, ваши тестовые скрипты будут стабильными и эффективными.

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

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

Например, представим такую ситуацию. Вы создаёте в приложении новую запись и нажимаете кнопку «Сохранить», после чего на экране появляется сообщение «Данные сохранены» с единственной кнопкой «ОК». Чтобы продолжить работу с приложением, необходимо закрыть окно, нажав на эту кнопку.

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

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

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

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

Самый простой способ ожидания – это использование функций типа Sleep() или Delay(), которые попросту ждут определенное время (1, 5, 100 секунд), после чего продолжают выполнение скрипта. Однако это – самый неэффективный способ, так как каждый раз задержки будут одинаковые, хотя ожидаемое событие может произойти как раньше, так и позже. В первом случае мы просто потеряем время на ненужное ожидание, во втором – получим ту же самую ошибку, какую получили бы без использования задержки, после чего придется еще больше увеличить ожидание, тем самым увеличивая время выполнения скрипта.

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

Для практической демонстрации рассмотрим следующий пример. Предположим, что в приложении Калькулятор нам необходимо ждать, пока откроется окно About. Как только оно откроется, мы попросту закроем его с помощью скрипта, а открывать его мы будем вручную в случайный момент. Максимальное время ожидания появления окна пусть будет 20 секунд.

Вот как выглядит неправильный пример:

function TestDelayWrong()
{
  var pCalc = Sys.Process("CalcPlus");
  pCalc.Window("SciCalc", "Calculator Plus").Activate();
  aqUtils.Delay(20000); // задержка на 20 секунд
  if(!pCalc.Window("*", "About*").Exists)
  {
    Log.Error("Window 'About' not found");
  }
  else
  {
    pCalc.Window("*", "About*").Close();
  }
}

Здесь мы ожидаем 20 секунд всегда, даже если окно About откроется через 1 секунду. А вот правильный пример:

function TestDelayCorrect()
{
  var pCalc = Sys.Process("CalcPlus");
  pCalc.Window("SciCalc", "Calculator Plus").Activate();
  
  if(!pCalc.WaitWindow("*", "About*", 1, 20000).Exists)
  {
    Log.Error("Window 'About' not found");
  }
  else
  {
    pCalc.Window("*", "About*").Close();
  }
}

В этом примере мы ждём максимум 20 секунд, однако если окно About появится раньше – тут же начнём с ним работать. Подобные Wait-методы в TestComplete есть и для других элементов управления. Кроме того, для ожидания того, что свойство принимает определенное значение, существует метод WaitProperty().

Аналогичные функции или методы существуют во многих инструментах автоматизации, если же такого нет – их нужно обязательно написать самим, иначе будет очень трудно писать эффективные автоматизированные скрипты. Чтобы написать подобную функцию, можно в цикле проверять свойство Exists элемента и возвращать True, если Exists=True, или False при выходе из цикла.

Здесь необходимо упомянуть о нескольких опасных моментах.

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

Например, пусть у нас есть абстрактная функция ожидания свойства Exists:

while(true)
{
  if(object.Exists)
  {
    return True;
  }
}

В этом случае обращение к объекты будет происходить очень часто (несколько тысяч раз в секунду) и процесс будет занимать 100% процессорного времени. Лучше этот пример переписать так:

while(true)
{
  if(object.Exists)
  {
    return True;
  }
  sleep(1000); // задержка на 1 секунду (1000 миллисекунд)
}

В этом примере есть еще одна проблема: бесконечный цикл. Если приложение зависло, этот цикл будет выполняться вечно, поэтому его стоит переписать примерно так:

var waittime = 20; // максимальное время ожидание в секундах
while(true)
{
  if(object.Exists)
  {
    return True;
  }
  sleep(1000); // задержка на 1 секунду (1000 миллисекунд)
  waittime -= 1;
  
  if(waittime < 0)
  {
    return False; // выход по таймауту
  }
}

Синхронизация также может быть полезна в следующей ситуации. Предположим, что мы работаем с нестабильным сетевым приложением, у которого в случайные моменты обрывается соединение с сервером, при этом на экране появляется окно с кнопкой «Подключиться заново». В этой ситуации (если позволяет инструмент автоматизации) можно создавать 2 различных потока. Первый из них будет запускать скрипты, а второй – следить, не появилось ли сообщение о разорванном соединении, при этом выполняя необходимые действия (в нашем случае – нажимая кнопку). Конечно, есть вероятность, что пока приложение будет подключаться, в другом потоке уже возникнет ошибка и текущий тест будет содержать ошибку, однако при этом приложение будет восстановлено для работы других тестов.

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

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

function getWindow(proc, arrWindows)
{
  var waitTime = 10; // максимальное время ожидания в секундах
  while(true)
  {
    for(var i = 0; i < arrWindows.length; i++)
    {
      if(proc.WaitWindow("*", arrWindows[i], 1, 1).Exists)
      {
        return i;
      }
    }
  waitTime -= 1;
  if(!waitTime)
  {
    return -1;
  }
    aqUtils.Delay(1000);
  }
  return -1;
}

function TestMultiWindowWait()
{
  var pNote = Sys.Process("notepad");
  var wNote = pNote.Window("Notepad", "*Notepad");

  wNote.Activate();
  var wndIndex = getWindow(pNote, ["Find", "About*"]);
  switch(wndIndex)
  {
    case 0: /*Find window*/
      pNote.Window("*", "Find").Window("Button", "Cancel").Click();
      break;
    case 1: /*About window*/
      pNote.Window("*", "About*").Window("Button", "OK").Click();
      break;
    default: /*other window or no window*/
      Log.Error("None of the expected windows appeared");
      break;
  }
}

Функция getWindow() – это та самая функция, которая в цикле перебирает окна заданного процесса и возвращает индекс найденного окна (или -1, если ни одно окно не было найдено), а в тесте TestMultiWindowWait мы выполняем действие, специфичное для конкретного окна.

Обратите также внимание, что использовать синхронизацию необходимо с умом, а не просто вставлять её повсюду. Так, например, нет смысла ждать появления кнопки OK в окне, если она там присутствует постоянно с самого начала существования окна, это уже будет ненужной перегрузкой тестов. Как и в любой задаче, здесь необходимо придерживаться «золотой середины».

2.8 Нелинейное тестирование

Большинство приложений (кроме, пожалуй, самых простых) позволяют выполнять одни и те же действия разными способами. Например, чтобы вызвать диалоговое окно открытия файла, можно выбрать пункт меню File – Open, нажать клавиатурную комбинацию Ctrl-o или выбрать пункт меню с помощью клавиатуры (Alt-f-o). Действия разные, а результат один. По хорошему проверять необходимо все варианты.

Другой пример – работа с данными. Предположим, у нас есть поле ввода, в которое мы можем ввести число от 1 до 100 (или выпадающий список с несколькими пунктами). Мы обязаны проверить данные из разных классов эквивалентности (например, что в поле нельзя ввести текст, отрицательные числа или числа больше 100, а также проверить граничные значения 1 и 100), однако внутри интервала от 2 до 99 мы можем выбирать любое число: так как они находятся в одном классе эквивалентности, нет смысла проверять их все. Однако существует такой тип ошибки data related, т.е. ошибки, связанные с неправильными данными. Например, программа будет работать корректно со всеми числами, кроме числа 59. Почему? Этого мы не знаем, например в базу данных попало неправильное значение, однако мы можем даже ни разу не наткнуться на эту ошибку, полагая, что программа ведёт себя одинаково со всеми числами больше единицы и меньше ста.

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

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

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

function randInt(min, max)
{
  return Math.round(Math.random()*(max-min)+min)
}

А теперь собственно функция, которая вызывает диалоговое окно Open в Блокноте либо случайным образом, либо заданным (если передать ей параметр).

function openFile(method)
{
  if(method == undefined)
  {
    method = randInt(1, 3);
    Log.Message("Random method selected: " + method);
  }

  var wNote = Sys.Process("notepad").Window("Notepad", "*Notepad");
  switch(method)
  {
    case 1: // by selecting menu item
      wNote.MainMenu.Click("File|Open...");
      break;
    case 2: // by pressing Ctrl-o
      wNote.Keys("^o");
      break;
    case 3: // by pressing Alt-f-o
      wNote.Keys("~fo");
      break;
    default: // unknown method
      Log.Error("Unknown method: " + method);
      break;
  }
}

В следующем примере мы будем бесконечно вызывать появление окна Open случайным образом.

function TestOpenFile()
{
  var wNote = Sys.Process("notepad").Window("Notepad", "*Notepad");
  wNote.Activate();
  while(true)
  {
    openFile();
    Sys.Keys("[Esc]");
  }
}

А если нам понадобится вызвать окно Open каким-то конкретным способом (например, через меню), нам достаточно вызвать функцию openFile() с соответствующим параметром.

openFile(1);

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

  • Сложность написания кода. Для каждого случая нам необходимо писать отдельный код (в нашем примере это всего одна строка, однако в реальных приложениях мы можем рандомизировать более сложные действия, чем просто выбор пункта меню).
  • Сложность воспроизведения ошибок. Если во время работы скрипта появилась какая-то ошибка, нам необходимо будет воспроизвести её вручную, для чего пройти все шаги теста в точности как это делал скрипт. Именно для этой цели в функции openFile() мы добавили строку, которая выводит в лог метод, выбранный случайным образом.
  • Сложность воспроизведения ситуации в автоматическом режиме. Если ошибка была найдена и исправлена программистами, нам необходимо её протестировать, однако мы не знаем заранее, каким именно путём пойдёт скрипт в очередной раз. Поэтому если ошибка некритичная, её достаточно проверить вручную и продолжать запускать скрипт, генерируя случайные данные. Если же ошибка критичная, можно написать отдельный скрипт, который тестирует именно этот путь, и постоянно запускать два теста: один по случайному пути и второй по жёстко заданному.

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

Всё сказанное выше относительно путей прохождения приложения, справедливо также и для генерации случайных данных.

Этот подход особенно хорошо работает при частых запусках тестов (например, после каждой сборки приложения или при ночных запусках), поэтому он хорошо подойдёт для smoke-тестирования, а вот для acceptance тестирования необходимо постараться проверить как можно больше путей. Но и тут можно оптимизировать процесс. Например, в рассмотренном случае для acceptance тестирования можно при вызове функции openFile() сначала проверить, что каждый из доступных способов вызывает появление на экране окна Open (каждый раз закрывая его – тоже разными способами), и лишь потом продолжить выполнение теста.

2.9 Выбор подходящей методологии

Вполне вероятно, что после прочтения этой главы у вас возникнет вопрос: «Так а какой же подход мне выбрать?». Чаще всего ответом на этот вопрос является такой: комбинируйте несколько подходов, которые вам нравятся, или которые подходят в вашем случае.

Действительно, практически не бывает проектов, в которых использовался бы лишь один подход, обычно приходится их комбинировать. Пожалуй, единственным подходом, который используется отдельно от остальных, является чистый Record & Play, т.е. такой подход, при котором вы просто записываете тесты и никак их не редактируете, а в случае возникновения проблем с каким-то тестом попросту перезаписываете этот скрипт. Однако, как мы объясняли выше, этот подход самый плохой из всего, что можно только представить. К этой же категории можно отнести и «простые» keyword-тесты.

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

Чем более сложное тестируемое приложение и чем сложнее ваши тесты – тем сложнее будет структура проекта. Бояться этого не нужно, скорее наоборот: стоит бояться сваливать функции в одну кучу с мыслью «позже разберусь». Старайтесь сразу же выносить функции в соответствующие модули. Лучше иметь модуль с одной единственной функцией, чем поместить эту функцию туда, где ее никто не станет искать.

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

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

Синхронизация выполнения также используется повсеместно, так как может понадобиться в любом, самом неожиданном, месте (в коде приложения или в коде тестовых скриптов).

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

«Сложные» keyword-тесты обычно используются в том случае, когда разработкой кода скриптов и тестов занимаются разные люди. При этом те, кто разрабатывают тесты, могут быть совершенно незнакомы с программированием вообще и со структурой проекта в частности; их задача – создание тестов, а работоспособность этих тестов ложится на плечи разработчиков скриптов.

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

Примечания:

[1] Если вы хотите выполнять примеры, рассмотренные в этом учебнике, вы можете скачать пробную 30-тидневную версию TestComplete на сайте http://smartbear.com/.

[2] На самом деле в TestComplete всё устроено несколько сложнее, однако для наших целей такого простого описания будет достаточно. Если вы хотите изучить TestComplete более подробно, вы можете, например, обратиться к бесплатному онлайн-руководству на этом сайте.

[3] Здесь и далее под «кодом приложения» мы подразумеваем код скриптов, который предназначен для работы с тестируемым приложением, а не «исходный код тестируемого приложения».

[4] Такой же результат можно получить не только в калькуляторе Windows, но и в «настоящих» настольных калькуляторах.

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