5. Пример создания проекта

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

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

В качестве тестируемого приложения мы возьмем стандартный Блокнот (приложение, идущее в поставке с Windows). В качестве объекта автоматизации мы выбрали Smoke-тест (т.е. поверхностный тест, задача которого – поверхностно проверить базовые функции приложения).

5.1 Постановка задачи

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

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

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

5.2 Создание тесткейсов

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

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

Теперь напишем шаги тесткейса.

Блокнот. Smoke-тест
Шаг № Действие Проверка
1 Запустите Блокнот из командной строки Блокнот запущен.

Главное окно активно.

Никаких дополнительных диалоговых окон не открылось

2 Проверьте, что все элементы меню, заканчивающиеся на «…», активны Все пункты меню активны
3 Выберите первый доступный элемент меню из предыдущего шага любым способом (клавиатура, мышь, горячая клавиша) Открывается диалоговое окно с заголовком, совпадающим с текстом меню
4 Закройте окно, открытое в предыдущем шаге, нажатием на кнопку закрытия [x] Диалоговое окно закрыто
5 Повторите шаги 3-4 для всех пунктов меню из шага 2 Тот же результат
6 Закройте Блокнот, выбрав пункт меню File|Exit Блокнот закрыт

 

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

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

5.3 Создание проекта и выбор подходов

Теперь определимся с тем, какие подходы мы будем использовать в наших тестах:

  • Мы, безусловно, воспользуемся функциональной декомпозицией (глава 2.2), так как ее необходимо использовать всегда (даже в таком простом случае, как наш пример). Для более удобной навигации по проекту мы будем размещать модули в соответсвующих папках проекта.
  • Мы воспользуемся object-driven подходом (глава 2.5) для написания кода работы с Блокнотом. Это также позволит нам не использовать стандартную возможность NameMapping, предоставляемую нам TestComplete’ом[1].
  • Мы будем использовать data-driven подход (глава 2.3) для хранения данных (пункты меню и соответствующие им окна, горячие клавиши и т.п.).
  • Для проверки открытия окон мы используем некоторые подходы синхронизации (глава 2.7).
  • И, наконец, мы воспользуемся нелинейным подходом (глава 2.8) для тестирования (будем обращаться к пунктам меню случайным образом: мышь, клавиатура, горячие клавиши).

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

5.4 Создание структуры кода и общих функций


Рис. 6.1. Общий вид проекта

Эта структура не была создана сразу, она наращивалась постепенно по мере создания общего кода для работы с приложением Блокнот.

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

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

И, наконец, в папку NotepadTests мы будем помещать непосредственно тесты.

Давайте рассмотрим каждый из файлов более подробно.

Constants

WAIT = 5000; // Wait timeout for windows

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

ExtDDT

function getDDTData(fileName, sheet)
{
  var data = {};
  var useACEdriver = (aqFileSystem.GetFileExtension(fileName) == "xlsx");
 
  var excel = DDT.ExcelDriver(fileName, sheet, useACEdriver);
  try
  {
    var i;
    for(i = 0; i < excel.ColumnCount; i++)
    {
      data[excel.ColumnName(i)] = [];
    }
  
    while(!excel.EOF())
    {
      for(i = 0; i < excel.ColumnCount; i++)
       {
         data[excel.ColumnName(i)].push(excel.Value(excel.ColumnName(i)));
       }
    excel.Next();
    }
  }
  catch(e)
  {
    Log.Error("An error occured while reading data from excel file",
    fileName + "\n" + sheet + "\n" + e.number + "\n" + e.description)
  }
  finally
  {
    DDT.CloseDriver(excel.Name);
  }
  
  return data;
}

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

StdLib

function arrayFind(arr, element)
{
 for (var i = 0; i < arr.length; i++)
 {
  if (element === arr[i])
  {
   return i;
  }
 }
 return -1;
}

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

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

TestDrive

var STEP = 1; // step number
var STEP_ID; // step id (used for creating folders in the log file)

function StartStep(description)
{
 STEP_ID = Log.CreateFolder("STEP #" + STEP + ": '" + description + "'");
 Log.PushLogFolder(STEP_ID);
}

function EndStep(expected)
{
 Log.PopLogFolder();
 Log.Message(" Expected: '" + expected + "'");
 STEP++;
}

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

Примеры использования всех этих функций мы увидим ниже при написании класса Notepad и smoke-теста.

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

NotepadClass

В нашем примере это самый большой файл. Здесь хранятся все методы для работы с Блокнотом, которые мы будем вызывать из тестов.

//USEUNIT Constants
//USEUNIT StdLib

function Notepad()
{
  // Properties
    // define custom properties here
    
  // Windows and Objects
    this.Process = function()
    {
      return Sys.Process("notepad");
    }
  
    this.MainWin = function()
    {
      return this.Process().Window("Notepad", "* - Notepad");
    }
    
    this.Dialog = function(wndCaption, wndClass)
    {
      if(wndClass == undefined)
      {
        wndClass = "#32770";
      }
      return this.Process().WaitWindow(wndClass, wndCaption, -1, WAIT);
    }
  
  // Methods
    this.run = function()
    {
      while(Sys.WaitProcess("notepad", 100).Exists)
      {
        Log.Message("Terminating running Notepad");
        Sys.Process("notepad").Terminate();
      }
      TestedApps.notepad.Run();
      if(!Sys.WaitProcess("notepad", WAIT).Exists)
      {
        Log.Error("Notepad process didn't start");
        return false;
      }
      return true;
    }
  
    this.close = function()
    {
      this.Process().Terminate();
    }
    
    this.getMenuItems = function()
    {
      var menus = [];
      var i, j;
      for(i = 0; i < this.MainWin().MainMenu.Count; i++)
      {
        var currentMenu = this.MainWin().MainMenu.Items(i);
        for(j = 0; j < currentMenu.SubMenu.Count; j++)
        {
          menus.push(currentMenu.Caption + "|" + currentMenu.SubMenu.Items(j).Caption);
        }
      }
      
      return menus;
    }
    
    this.selectMenuItem = function(menu)
    {
      var method = randInt(1, 3);
      Log.Message("Selecting menu item " + menu["MenuItem"]);
      Log.Message("Random method generated: " + method);
      
      switch(method)
      {
        case 1: // select by mouse
          this.MainWin().MainMenu.Click(menu["MenuItem"]);
          break;
        case 2: // select by accelerator
          this.MainWin().Keys(menu["Accelerator"])
          break;
        case 3: // select by hotkey
          if(menu["Hotkey"] != "")
          {
            this.MainWin().Keys(menu["Hotkey"]);
          }
          else
          {
            Log.Message("Hotkey isn't available, using accelerator");
            this.MainWin().Keys(menu["Accelerator"]);
          }
          break;
        default:
          Log.Error("Unknown method for selecting menu item: " + method);
          break;
      } 
    }
}

Обратите внимание, что каждый метод, выполняющий действие (например, close, run и т.д.) начинается с глагола, а все методы, возвращающие объекты (Dialog, MainWin), этому правилу не следуют. Это сделано для того, чтобы всегда придерживаться определенных стандартов и точно знать, что делает тот или иной метод. Мы также придерживаемся правила начинать имена методов с маленькой буквы, если этот метод выполняет действие, и с большой – если он возвращает объект. Это также улучшает и упрощает чтение кода.

5.5 Написание автоматических тесткейсов

Теперь перейдем к написанию автотестов. В нашем случае это всего один Smoke-тест.

SmokeTests

//USEUNIT NotepadClass
//USEUNIT ExtDDT
//USEUNIT TestDrive

function TestSmokeDialogs()
{
  StartStep("Start Notepad application")
    var np = new Notepad();
    np.run();
  EndStep("Notepad is started");
  
  StartStep("Make sure that Smoke test covers all existing dialog boxes")
    var expMenus = getDDTData(Project.Path + "\\Data\\NotepadData.xlsx", "SmokeTestData");
    var actMenus = np.getMenuItems();
    
    for(var i = 0; i < actMenus.length; i++)
    {
      if(actMenus[i].substr(actMenus[i].length-3, 3) == '...' &&
          arrayFind(expMenus["MenuItem"], actMenus[i]) == -1)
      {
        Log.Warning("Menu item '" + actMenus[i] + "' not tested");
      }
    }
  EndStep("All dialog boxes are being tested");         
  
  StartStep("Verify all available dialogs can be opened and closed");
    for(var i = 0; i < expMenus["MenuItem"].length; i++)
    {
      var menu = {
                  MenuItem: expMenus["MenuItem"][i],
                  Accelerator: expMenus["Accelerator"][i],
                  Hotkey: expMenus["Hotkey"][i] 
                  };
      
      if(expMenus["WindowCaption"][i] == "Find")
      {
        np.MainWin().Keys("Some text to enable menu Find");
      }
      np.selectMenuItem(menu);
      var dialog = np.Dialog(expMenus["WindowCaption"][i]);
      if(dialog.Exists)
      {
        dialog.Close();
      }
      else
      {
        Log.Error("Window '" + expMenus["WindowCaption"][i] + "' didn't open");
        np.close();
        np.run();
      }
    }        
  EndStep("All dialogs can be opened and closed");
  
  StartStep("Close Notepad application")
    np.MainWin().Activate();
    np.close();
  EndStep("Notepad is closed");      
}

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

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

Хотя написание тестов мы и выделили в отдельный раздел, все 3 части нашего проекта (Common, Notepad и NotepadTests) писались одновременно, постепенно дополняя и расширяя друга.

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

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

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

5.6 Настройка регулярных запусков тестов

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


Рис. 6.2. Лог запуска теста

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


Рис. 6.3. Лог с ошибкой

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

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

Независимо от того, как и где мы будем запускать наши тесты, нам понадобится какой-то простой способ это делать. Проще всего воспользоваться возможностями batch-файлов. Ниже представлен листинг файла RunSmokeTest.bat, который будет использоваться для запуска нашего Smoke-теста.

@echo off
taskkill /fi "STATUS eq RUNNING" /f /t /im testcomplete.exe

set TC_PATH=C:\Program Files\Automated QA\TestComplete 7\Bin\TestComplete.exe

"%TC_PATH%" .\ProjectSuite.pjs /e /r /p:NotepadDemo /u:SmokeTests /rt:TestSmokeDialogs

if %ERRORLEVEL% == 0 goto end
if %ERRORLEVEL% == 1 goto warning
if %ERRORLEVEL% == 2 goto error

:error
echo Critical error(s) were found during script run
exit /b 2

:warning
echo Warning(s) were found during script run, sending e-mail
call .\send_email.bat
goto end

:end
echo Tests ran successfully

Прежде всего мы закрываем процесс TestComplete’a (если он вдруг открыт), затем запускаем тест (передав его в качестве параметра в TestComplete), а в конце проверяем возвращаемое TestComplete’ом значение и в зависимости от его значения выполняем необходимые действия. Если TestComplete вернул 0, значит всё в порядке, ошибок не было. Если он вернул 1, значит в тесте были предупреждения. Этого недостаточно, чтобы считать билд нерабочим, поэтому мы отсылаем письмо (запустив файл send_email.bat) и затем выходим из пакетного файла с кодом 0 (чтобы билд считался успешным). Если же TestComplete вернул нам 2, значит при выполнении теста возникли ошибки, поэтому из пакетного файла мы выходим с кодом 2, чтобы сообщить, что что-то не в порядке (по умолчанию код выхода 0 считается успешным окончанием, любой другой – ошибкой).

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

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

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

5.7 Заключение

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

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

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

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

Желаем удачи в изучении автоматизации и постановке процессов в ваших проектах!

Примечания:

[1] Это вовсе не значит, что NameMapping плох. Просто в этом учебнике мы стараемся избегать специальных возможностей инструментов, чтобы не пришлось сильно отвлекаться на их объяснение.

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