Об асинхронности и модальности в GUI тестах
О тестах GUI я писал тут - http://18delphi.blogspot.ru/2013/11/gui.html ну и "в последующих сериях".
Сегодня я размышлял над одной хорошей заметкой моего товарища по цеху - Романа Янковского.
О Awaitable-значениях (http://roman.yankovsky.me/?p=1100).
И неожиданно понял - что ещё я хочу сказать о GUI-тестировании.
Есть два момента с которыми рано или поздно сталкиваются GUI-тестировщики. Когда система входит в такое состояние, когда она блокирует управление и крутит какой-то "внутренний цикл".
А именно:
1. Например показ контекстного меню или что-то подобное.
2. Показ модального диалога и ожидание ввода пользователя.
И это далеко не самая простая проблема.
Ведь код теста - "замораживается" и система "ждёт ввода от пользователя" и тесты на это повлиять никак уже не могут.
Им банально - не отдаётся управление. И они не могут продолжать свою работу.
А ведь - они автоматические и никакой пользователь им не поможет.
Что же делать?
У нас тоже была такая проблема и мы её решили.
Мы ввели два слова тестовой машины - THREAD и MODAL.
Формат их таков:
"По-русски" это выглядит так:
Приведу "развесистый" набор примеров, чтобы интересующиеся "прониклись".
Дальше реально будет "хардкор". Но он именно для "особых ценителей".
Примеры использования THREAD в нашей тестовой машине:
Теперь о том, как "это устроено".
Код доступен тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/ScriptEngine/
Приведу часть кода.
Он - большой. И НЕ компилируется. Но он - для ОСОБЫХ ценителей.
Код для слова THREAD:
Код слова MODAL:
Засим - я закончу.
Ибо "разжёвывать" - не входило в план этой заметки.
Это всё же - "поток сознания", а не "учебник".
Главное было - обрисовать проблемы и указать на то, что они решаемы вообще и решены хотя бы в одном "частном случае".
О тестах 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.
Засим - я закончу.
Ибо "разжёвывать" - не входило в план этой заметки.
Это всё же - "поток сознания", а не "учебник".
Главное было - обрисовать проблемы и указать на то, что они решаемы вообще и решены хотя бы в одном "частном случае".
Комментариев нет:
Отправить комментарий