четверг, 28 ноября 2013 г.

Об асинхронности и модальности в GUI тестах

Об асинхронности и модальности в GUI тестах

О тестах GUI я писал тут - http://18delphi.blogspot.ru/2013/11/gui.html ну и "в последующих сериях".

Сегодня я размышлял над одной хорошей заметкой моего товарища по цеху - Романа Янковского.

О Awaitable-значениях (http://roman.yankovsky.me/?p=1100).

И неожиданно понял - что ещё я хочу сказать о GUI-тестировании.

Есть два момента с которыми рано или поздно сталкиваются GUI-тестировщики. Когда система входит в такое состояние, когда она блокирует управление и крутит какой-то "внутренний цикл".

А именно:

1. Например показ контекстного меню или что-то подобное.
2. Показ модального диалога и ожидание ввода пользователя.

И это далеко не самая простая проблема.

Ведь код теста - "замораживается" и система "ждёт ввода от пользователя" и тесты на это повлиять никак уже не могут.

Им банально - не отдаётся управление. И они не могут продолжать свою работу.

А ведь - они автоматические и никакой пользователь им не поможет.

Что же делать?

У нас тоже была такая проблема и мы её решили.

Мы ввели два слова тестовой машины - THREAD и MODAL.

Формат их таков:

THREAD ( 
 код который выполняется внутри отдельного потока выполнения 
)

@ ( 
    код который приводит к вызову модального диалога 
  ) MODAL 
  ( 
    код который выполняется внутри run-loop цикла модального диалога 
  )

"По-русски" это выглядит так:

"Выполнить асинхронно" ( 
 код который выполняется внутри отдельного потока выполнения 
)

"Выполнить действия {(@ ( код который приводит к вызову модального диалога ) )} и обработать модальность следующим образом" (
 код который выполняется внутри run-loop цикла модального диалога 
)

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

Дальше реально будет "хардкор". Но он именно для "особых ценителей".

Примеры использования THREAD в нашей тестовой машине:

: K296095220
 : Сохраняем_размеры_навигатора
  "Открываем НК"
  150 "Левый навигатор" "Установить ширину"
  "Перевести фокус в оглавление"
  "Сделать оглавление неплавающим"
  "Сделать левый навигатор автораспахивающимся"
  //10 -10 "Найти главное окно" pop:control:ClientToScreen mouse:SetCursorPosition
  "Установить курсор мыши по координатам {(10 -10)} относительно контрола {("Найти главное окно")}"
  THREAD (  6650 SLEEP "Клик левой кнопкой мыши" )
  THREAD (  700 SLEEP "Провести мышью вниз на {(360)} пикселей" 
  "Левый навигатор" "Померить ширину" .  )
  "Зажать левую кнопку мыши"
 ;

 : Действия
  "Запомнить ширину левого навигатора и выполнить {(@ Сохраняем_размеры_навигатора )}"
 ;

 TRY
  "Запомнить позицию мыши и выполнить {(@ Действия )}"
 FINALLY 
  "Перевести фокус в оглавление"
  "Сделать левый навигатор НЕавтораспахивающимся"
 END
; // K296095220

: K321983826

: "Закрыть второе окно и переключиться на первое"
 VAR l_Main 0 >>> l_Main
 "Сохранить активное окно" >>> l_Main 
 l_Main "Закрыть сохраненное окно"
;

 : Сценарий_теста
  "Открыть НК"
  "Список редакций"
  "Открыть текущую редакцию в новом окне"
  СР
  TRY
   : Действия
    "Установить курсор мыши по координатам {( 10 50 )} относительно контрола {("Контрол в фокусе")}"
    THREAD ( 800 SLEEP 
    "Установить курсор мыши по координатам {( 40 145 )} относительно контрола {("Контрол в фокусе")}"
    "Клик левой кнопкой мыши")
    "Клик правой кнопкой мыши"
   ; // Действия
 
   "Вызвать Список редакций из статусной строки и сделать {(@ Действия)}"
  FINALLY
   "Закрыть второе окно и переключиться на первое"
   "Дать системе перерисоваться" 
  END
 ; // Сценарий_теста 

 "Запомнить позицию мыши и выполнить {(@ Сценарий_теста )}"
; // K321983826

: K342853493

 : Действия
  "Установить фокус в баллон зелёной медали МВ"
  "Установить курсор мыши на правый край текущего редактора" 
  INTEGER VAR X
  INTEGER VAR Y
  "Запомнить позицию мыши" =: Y =: X
  "Установить курсор мыши на левый край текущего редактора"
  THREAD ( 300 SLEEP 
           X Y "Восстановить позицию мыши"
           100 SLEEP
           "Отпустить левую кнопку мыши" )
  "Зажать левую кнопку мыши"
  "Сравнить выделенный текст текущего редактора с эталоном"
 ;

 ТБ24
 "Открываем {(900100)}"
 "Устанавливаем дату машины времени {('01.10.2003')}"
 "Нажать на зелёную медаль МВ и проверить наличие баллона"
 "Запомнить позицию мыши и выполнить {(@ Действия)}"
; // K342853493

: K377162741
 
 "Открываем НК"
 THREAD ( 3000 SLEEP "Закрыть F1." )
 "Предварительный просмотр"
 "Список всех документов"
 ОМ
; // K377162741

: K377163047

 ТБ24
 "Открываем НК"
 TRY
  "Выделить всё"
  THREAD ( 3000 SLEEP ТБ27 ) 
  "Сохранить выделенный текст в формате {(CF_RTF)}"
 FINALLY 
  3000 SLEEP
  ТБ24
 END 
; // K377163047

: K390561811

 : Сохраняем_позицию_курсора
  : Тест
   : Действия
    "Нажать {('Enter')}"
    "Открыть список документов с комментариями"
    "Нажать {('Enter')}"
    "Стрелка вниз"
    THREAD ( 600 SLEEP
    "Дождаться переключения вкладок"
    "Ставим указатель мыши на конец текущего параграфа редактора  {("Контрол в фокусе")} со смещением {( 10 10 )}"
    "Клик левой кнопкой мыши" )
    // убираем вызванное контекстное меню
    "Ставим указатель мыши на конец текущего параграфа редактора  {("Контрол в фокусе")} со смещением {( 0 0 )}"
    "Клик правой кнопкой мыши"
    // для повторения ошибки достаточно было вызвать контекстное меню
    ; // Действия 
   "Сделать {(@ Действия )} с комментариями в документе {(10003000)}" 
  ; // Тест
 
  // анти-тест Тест
  Тест

 ; // Сохраняем_позицию_курсора

 "Вывести окно оболочки на первый план"
 "Запомнить позицию мыши и выполнить {(@ Сохраняем_позицию_курсора )}"
; // K390561811

: K390568127

: "Кликаем кнопку на координатах , и кликаем выпадающий список по координатам" IN aControl  INTEGER IN X INTEGER IN Y  INTEGER IN Xx INTEGER IN Yy

 : Действия
  OBJECT VAR "Нужная кнопка"
  "Найти контрол {(aControl)} на форме {("Найти главное окно")}" =: "Нужная кнопка" 
  "Нужная кнопка" "Узнать, существует ли объект" ! 'Не нашли кнопку' ASSERTS
     THREAD ( 1000 SLEEP 
       "Установить курсор мыши по координатам {( Xx Yy )} относительно контрола {("Нужная кнопка" )}"
       "Клик левой кнопкой мыши" )
     THREAD (  500 SLEEP 
        "Установить курсор мыши по координатам {( X Y )} относительно контрола {("Нужная кнопка" )}"
        "Клик левой кнопкой мыши" )
   4000 SLEEP
   "Дождаться переключения вкладок"
   'Down'  "Нужная кнопка"  "Узнать численную переменную объекта" 0 ?!= .
  ; // Действия
 "Запомнить позицию мыши и выполнить {(@ Действия)}"
 OnTest
; // "Кликаем кнопку на координатах, и кликаем выпадающий список по координатам" 

 : Повторяем_ошибку
  "Кликаем кнопку {('bt_mo_Common_OpenMainMenuNew')} на координатах {( 108 10 )}, и кликаем выпадающий список по координатам {( 10 90)}"
 ; // Повторяем_ошибку

 "Переместить медали по координатам {( 600 10 )} и выполнить {(@ Повторяем_ошибку )}"
; // K390568127

: K392168746

   : Проверяем_статусбар OBJECT IN l_It 
   l_It "Убедиться, что контрол активен" IF
    STRING VAR str1
    STRING VAR str2
    CONST cSeparator '...'
    l_It "Заголовок контрола" >>> str1
    ( str1 "НЕ РАВНО" '' ) IF
      str1 cSeparator string:Split =: str2 =: str1
       ( str1 РАВНО 'Подсчет числа страниц' ) IF
         [[ str1 '... Надпись ЕСТЬ'  ]] strings:Cat >>> str1
         str1 .
       ENDIF
    ENDIF
   ENDIF 
  ; // Проверяем_статусбар

 "Очистить историю"
 "Открываем {(12044185)}"
 THREAD ( 500 SLEEP
 "Для всех видимых элементов статусбара {(контрол::StatusBar:push)} выполнить {(@ Проверяем_статусбар )}" )
 "Предварительный просмотр"
; // K392168746

Примеры использования MODAL в нашей тестовой машине:

: K227478809

 ППР
 "Установить фокус в поле 'Раздел/Тема' "
 "Выбрать в дереве атрибутов контекст {('Бухгалтерский учет, аудит, статистическая отчетность')}"
 "Нажать Искать"
 "Назад по истории"
 "Установить фокус в поле 'Раздел/Тема' "
 "Добавить ещё один атрибут"
 @ ( "Нажать {('Enter')}" ) MODAL (
 "Стрелка вниз" 
 "Нажать {('Enter')}"
 "Выбрать атрибут ИЛИ" )
 "Нажать Искать"
 "Назад по истории"
 "Установить фокус в поле 'Раздел/Тема' "
 "Выделить текст в поле"
 "Сравнить выделенный текст текущего редактора с эталоном"
 "Стрелка вниз"
 "Выделить текст в поле"
 "Сравнить выделенный текст текущего редактора с эталоном"
; // K227478809

: K278833441

 : "В тексте слева кликаем по ссылке внизу 'Список редакций, открываемых по скрипту'"
   "Перевести фокус в левую редакцию"
   "В конец документа"
   "Ставим указатель мыши на конец текущего параграфа редактора {("Контрол в фокусе")} со смещением {(0 -30)}"
   "Узнать индекс курсора мыши" РАВНО '0' IF
   @ ( "Клик левой кнопкой мыши" ) MODAL ( "Перевести фокус в дерево редакций" "Получить имя текущего элемента дерева {(контрол::RedactionTree)}" )
   ELSE
   'Почему-то не попали на ссылку' .
   ENDIF  
 ; // "В тексте слева кликаем по ссылке внизу 'Список редакций, открываемых по скрипту'"

  : Действия
   "Открываем {(8901001)}"
   СР
   "В тексте слева кликаем по ссылке внизу 'Список редакций, открываемых по скрипту'"
  ; // Действия

  : Ставим_размеры_окну
   "Запомнить позицию мыши и выполнить {(@ Действия )}"
  ;

 : Восстанавливаем_состояние_окна 
  "Выставить форме размеры {( 900 700 )} и {(@ Ставим_размеры_окну )}"
 ;

 "Сделать {(@ Восстанавливаем_состояние_окна )} с изменением состояния и размеров окна {("Найти главное окно")}"
; // K278833441

: K96480889

  "Восстановить все настройки текущей конфигурации"
 @ "Настройка конфигурации" MODAL ( "Проверить доступность кнопки {('bt_enResult_opRestoreAllSettings')} в модальном окне"
 "Нажать кнопку Отмена в Настройке конфигурации" )
; // K96480889

: K87196896

 : "Померить высоту и ширину модального окна"
  OBJECT VAR "Диалог ввода номера"
  focused:control:push pop:control:GetAnotherParentForm =: "Диалог ввода номера"
  "Диалог ввода номера" "Померить высоту" .
  "Диалог ввода номера" "Померить ширину" .
 ;

 @ ( "Нажать {('Alt+N')}" ) MODAL (  "Померить высоту и ширину модального окна" )
; // K87196896

Теперь о том, как "это устроено". 

Код доступен тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/ScriptEngine/

Приведу часть кода.

Он - большой. И НЕ компилируется. Но он - для ОСОБЫХ ценителей.

Код для слова THREAD:

unit kwTHREAD;

interface

uses
  tfwScriptingInterfaces,
  kwCompiledWord,
  kwCompiledWordWorker,
  l3Interfaces,
  l3ParserInterfaces
  ;

type
 {$Include ..\ScriptEngine\tfwWordWorker.imp.pas}
 TkwTHREAD = {final} class(_tfwWordWorker_)
 protected
 // realized methods
   function CompiledWorkerClass: RkwCompiledWordWorker; override;
 public
 // overridden public methods
   class function GetWordNameForRegister: AnsiString; override;
 end;//TkwTHREAD

implementation

uses
  kwCompiledThread,
  l3Parser,
  kwInteger,
  kwString,
  SysUtils,
  TypInfo,
  l3Base,
  kwIntegerFactory,
  kwStringFactory,
  l3String,
  l3Chars,
  tfwAutoregisteredDiction,
  tfwScriptEngine
  ;

type _Instance_R_ = TkwTHREAD;

{$Include ..\ScriptEngine\tfwWordWorker.imp.pas}

// start class TkwTHREAD

function TkwTHREAD.CompiledWorkerClass: RkwCompiledWordWorker;
begin
 Result := TkwCompiledThread;
end;//TkwTHREAD.CompiledWorkerClass

class function TkwTHREAD.GetWordNameForRegister: AnsiString;
 {-}
begin
 Result := 'THREAD';
end;//TkwTHREAD.GetWordNameForRegister

initialization
 {$Include ..\ScriptEngine\tfwWordWorker.imp.pas}

end.

unit kwCompiledThread;

{$Include ..\ScriptEngine\seDefine.inc}

interface

uses
  kwCompiledWordWorker,
  tfwScriptingInterfaces
  ;

type
 TkwCompiledThread = class(TkwCompiledWordWorker)
 protected
 // realized methods
   procedure DoDoIt(const aCtx: TtfwContext); override;
 end;//TkwCompiledThread

implementation

uses
  seThreadSupport
  ;

// start class TkwCompiledThread

procedure TkwCompiledThread.DoDoIt(const aCtx: TtfwContext);
begin
 TseWorkThreadList.Instance.AddAndResumeThread(aCtx, f_Word);
end;//TkwCompiledThread.DoDoIt

end.

unit seThreadSupport;

{$Include ..\ScriptEngine\seDefine.inc}

interface

uses
  Classes,
  l3ProtoDataContainer,
  tfwScriptingInterfaces,
  l3Types,
  l3Memory,
  l3Interfaces,
  l3Core,
  l3Except
  ;

type
 TWordThread = class(TThread)
 private
 // private fields
   f_Context : TtfwContext;
   f_Word : TtfwWord;
 protected
 // realized methods
   procedure Execute; override;
 public
 // overridden public methods
   destructor Destroy; override;
 end;//TWordThread

 _ItemType_ = TWordThread;
 _l3ObjectPtrList_Parent_ = Tl3ProtoDataContainer;
 {$Define l3Items_IsProto}
 {$Include w:\common\components\rtl\Garant\L3\l3ObjectPtrList.imp.pas}
 TseWorkThreadList = class(_l3ObjectPtrList_)
 public
 // public methods
   class function WasInit: Boolean;
   procedure WaitForAllThreads;
   procedure AddAndResumeThread(const aContext: TtfwContext;
     const aWord: TtfwWord);
 public
 // singleton factory method
   class function Instance: TseWorkThreadList;
    {- возвращает экземпляр синглетона. }
 end;//TseWorkThreadList

implementation

uses
  SysUtils,
  l3Base {a},
  l3MinMax,
  RTLConsts
  ;

// start class TWordThread

procedure TWordThread.Execute;
begin
 f_Word.DoIt(f_Context);
end;//TWordThread.Execute

destructor TWordThread.Destroy;
begin
 FreeAndNil(f_Word);
 Finalize(f_Context);
 inherited;
end;//TWordThread.Destroy

// start class TseWorkThreadList

var g_TseWorkThreadList : TseWorkThreadList = nil;

procedure TseWorkThreadListFree;
begin
 l3Free(g_TseWorkThreadList);
end;

class function TseWorkThreadList.Instance: TseWorkThreadList;
begin
 if (g_TseWorkThreadList = nil) then
 begin
  l3System.AddExitProc(TseWorkThreadListFree);
  g_TseWorkThreadList := Create;
 end;
 Result := g_TseWorkThreadList;
end;

type _Instance_R_ = TseWorkThreadList;

{$Include w:\common\components\rtl\Garant\L3\l3ObjectPtrList.imp.pas}

// start class TseWorkThreadList

class function TseWorkThreadList.WasInit: Boolean;
begin
 Result := g_TseWorkThreadList <> nil;
end;//TseWorkThreadList.WasInit

procedure TseWorkThreadList.WaitForAllThreads;
var
 l_Thread : TWordThread;
begin
 while (Count > 0) do
 begin
  try
   l_Thread := Items[0];
   try
    l_Thread.WaitFor;
   except
   end;//try..except 
   l_Thread.Free;
   Delete(0);
  except
  end;//try..except
 end; // for i := 0 to Count - 1 do
end;//TseWorkThreadList.WaitForAllThreads

procedure TseWorkThreadList.AddAndResumeThread(const aContext: TtfwContext;
  const aWord: TtfwWord);
var
 l_Thread: TWordThread;
begin
 l_Thread := TWordThread.Create(True);
 aWord.SetRefTo(l_Thread.f_Word);
 l_Thread.f_Context := aContext;
 l_Thread.f_Context.rEngine := l_Thread.f_Context.rEngine.Clone;
 l_Thread.Resume;
 Add(l_Thread);
end;//TseWorkThreadList.AddAndResumeThread

end.

Код слова MODAL:

unit kwMODAL;

{$Include ..\ScriptEngine\seDefine.inc}

interface

uses
  tfwScriptingInterfaces,
  kwCompiledWord,
  kwCompiledWordWorker,
  l3Interfaces,
  l3ParserInterfaces
  ;

type
 {$Include ..\ScriptEngine\tfwWordWorker.imp.pas}
 TkwMODAL = {final} class(_tfwWordWorker_)
 protected
 // realized methods
   function CompiledWorkerClass: RkwCompiledWordWorker; override;
 public
 // overridden public methods
   class function GetWordNameForRegister: AnsiString; override;
 end;//TkwMODAL
 
implementation

uses
  kwCompiledModal,
  l3Parser,
  kwInteger,
  kwString,
  SysUtils,
  TypInfo,
  l3Base,
  kwIntegerFactory,
  kwStringFactory,
  l3String,
  l3Chars,
  tfwAutoregisteredDiction,
  tfwScriptEngine
  ;

type _Instance_R_ = TkwMODAL;

{$Include ..\ScriptEngine\tfwWordWorker.imp.pas}

// start class TkwMODAL

function TkwMODAL.CompiledWorkerClass: RkwCompiledWordWorker;
begin
 Result := TkwCompiledModal;
end;//TkwMODAL.CompiledWorkerClass

class function TkwMODAL.GetWordNameForRegister: AnsiString;
 {-}
begin
 Result := 'MODAL';
end;//TkwMODAL.GetWordNameForRegister

initialization
 {$Include ..\ScriptEngine\tfwWordWorker.imp.pas}

end.

unit kwCompiledModal;

interface

uses
  kwCompiledWordWorker,
  tfwScriptingInterfaces
  ;

type
 TkwCompiledModal = class(TkwCompiledWordWorker)
 protected
 // realized methods
   procedure DoDoIt(const aCtx: TtfwContext); override;
 end;//TkwCompiledModal

implementation

uses
  seModalSupport
  ,
  afwAnswer
  ;
  
// start class TkwCompiledModal

procedure TkwCompiledModal.DoDoIt(const aCtx: TtfwContext);
var
 l_Count : Integer;
begin
 l_Count := seAddModalWorker(f_Word, aCtx);
 try
  try
   (aCtx.rEngine.PopObj As TTfwWord).DoIt(aCtx);
  except
   on EvcmTryEnterModalState do
    Exit;
  end;//try..except
 finally
  RunnerAssert(seIsValidModalWorkersCount(l_Count), 'Видимо не выполнился код модального окна', aCtx);
 end;//try..finally
end;//TkwCompiledModal.DoDoIt

end.

unit seModalSupport;

{$Include ..\ScriptEngine\seDefine.inc}

interface

uses
  tfwScriptingInterfaces
  ;

function SeAddModalWorker(aWorker: TtfwWord;
  const aCtx: TtfwContext): Integer;
function SeExecuteCurrentModalWorker: Boolean;
function SeHasModalWorker: Boolean;
function SeIsValidModalWorkersCount(aCount: Integer): Boolean;

implementation

uses
  afwFacade,
  kwCompiledWord,
  seModalWorkerList
  ,
  afwAnswer
  ,
  seModalWorker
  ;

function SeAddModalWorker(aWorker: TtfwWord;
  const aCtx: TtfwContext): Integer;
begin
 TseModalWorkerList.Instance.Add(TseModalWorker_C(aWorker, aCtx));
 Result := TseModalWorkerList.Instance.Count;
end;//SeAddModalWorker

function SeExecuteCurrentModalWorker: Boolean;
var
 l_W : TseModalWorker;
begin
 Result := false;
 if not g_BatchMode then
  Exit;
 if TseModalWorkerList.Instance.Empty then
  Exit;
 afw.ProcessMessages; 
 l_W := TseModalWorkerList.Instance.Last;
 TseModalWorkerList.Instance.Delete(Pred(TseModalWorkerList.Instance.Count));
 l_W.rWord.DoIt(l_W.rContext^);
 afw.ProcessMessages; 
 Result := true;
end;//SeExecuteCurrentModalWorker

function SeHasModalWorker: Boolean;
begin
 Result := not TseModalWorkerList.Instance.Empty;
end;//SeHasModalWorker

function SeIsValidModalWorkersCount(aCount: Integer): Boolean;
begin
 Result := (TseModalWorkerList.Instance.Count < aCount);
 // - проверяем, что предыдущий модальный код выполнися
 if not Result then
  TseModalWorkerList.Instance.Delete(Pred(TseModalWorkerList.Instance.Count));
  // - снимаем этот код со стека, если он не выполнился
end;//SeIsValidModalWorkersCount

end.

Засим - я закончу.

Ибо "разжёвывать" - не входило в план этой заметки.

Это всё же - "поток сознания", а не "учебник".

Главное было - обрисовать проблемы и указать на то, что они решаемы вообще и решены хотя бы в одном "частном случае".

Комментариев нет:

Отправка комментария