Отладка приложений

         

Бич блочного тестирования: интерфейсы пользователя


Я твердо убежден, что разработчики Microsoft получают туннельный синдром запястья не от того, что им приходится вводить исходный код с клавиатуры, а от многократного нажатия одних и тех же комбинаций клавиш при тестировании своих приложений. После 5000-го нажатия <Alt>+<F>, <О> запястья зажаты плотнее, чем арматура в бетоне. Без инструмента автоматизации задач, имеющих доступ к различным свойствам ваших приложений, вообще приходится следовать некоторому сценарию, чтобы гарантировать выполнение блочного тестирования в достаточном объеме. Тестирование со сценариями — чрезвычайно скучная процедура, которая оставляет множество лазеек для человеческих ошибок.

Автоматизация блочных тестов означает уменьшение количества ввода с клавиатуры и уменьшение затрат времени на проверку состояния кода. К сожалению, приложение Recorder, которое обычно поставлялось с Windows 3.x, не включается в состав 32-разрядных операционных систем. Recorder обеспечивал запись действий пользователя (с мышью и клавиатурой) и последующее их воспроизведение, как если бы они были событиями физической мыши и клавиатуры (правда на его возможности воспроизведения перемещений мыши были наложены очень неудобные ограничения). Сейчас доступны продукты независимых поставщиков, предназначенные для автоматизации приложений и других работ (например, полной проверки каждого пиксела при сравнении экранов, поддержки служебных баз данных, связанных с тестированием, и т. д.), но требовалось создать нечто более удобное и предназначенное специально для инженеров-разработчиков. Так родилась идея этого приложения.

Когда я решил создать автоматизированную утилиту, то потратил некоторое время на уточнение того, что же можно ожидать от такого инструмента. Прежде всего, я подумал о разработке утилиты, похожей на старое приложение Recorder. В далекие времена Windows 3.x имелся полный набор REC-файлов для проведения тестирования. Однако у этого приложения был большой недостаток — оно не могло выполнять условные тесты.
Если во время тестирования приложение сообщало об ошибке, то Recorder шел по самому легкому пути — он просто воспроизводил зарегистрированные нажатия клавиш и щелчки мыши, полностью забывая о бедствии приложения. Однажды я стер половину операционной системы, потому что тестировал расширение WINFILE.EXE, и когда в нем возникли ошибки, Recorder выполнил последовательность удаления файлов для всего каталога \System. Новый автоматизированный инструмент определенно должен поддерживать конструкцию

if. . .then. . .else.

Чтобы включить в тесты условные конструкции, необходимо было использовать некоторый вид языка. Разработка собственного языка тестирования была бы увлекательным интеллектуальным упражнением, но вскоре я решил, что больше заинтересован в написании полезного инструмента отладки, чем в проектировании языка и возне с компиляторами YACC и FLEX. Потребовалось всего две секунды, чтобы понять, что надо написать Tester как СОМ-объект — таким образом, разработчики смогут использовать эту утилиту, а для написания тестов выбрать любой язык; я при этом мог сконцентрироваться на программировании свойств регрессивного тестирования (regression-testing) утилиты вместо проектирования нового языка. В качестве языков тестирования были выбраны языки сценариев типа Microsoft Visual Basic Scripting Edition (VBScript) и Java Script (JScript), потому что сценарии тестирования не требуют компиляции. Однако различные реализации машин сценариев Scripting Host (WSH) имеют несколько ограничений, которые будут рассмотрены чуть позже. Пока же поговорим о требованиях, которые привели меня к созданию утилиты Tester.



Что делать дальше с утилитой Tester?


Как уже говорилось, Tester делает хорошо только одну вещь: воспроизводит нажатия клавиш. Как и для всех утилит в этой книге, мы приветствуем поиск путей усовершенствований Tester'a, если вы, конечно, имеете склонность к этому. Вот некоторые возможные усовершенствования, которые можно попытаться реализовать:



добавьте классы-оболочки, такие как TListBox, TTreeControl и TRadioButton, чтобы получить возможность проверять различные состояния и содержимое элементов управления (чтобы они содержали надлежащие данные). С помощью этих классов можно начать проверку элементов управления и составление более сложных сценариев;  добавьте приложение, записывающее нажатия клавиш (recorder application). Попытайтесь сделать его расширяемым, чтобы при добавлении дополнительных классов-оболочек можно было с его помощью генерировать улучшенные сценарии;  исследуйте способы добавления к Tester'y ввода мыши. Можно было бы попытаться записывать расположение указателя мыши в процентных долях его смещения от краев экрана. Единственная проблема здесь состоит в том, что, изменяя разрешение экрана, следует пропускать элемент управления, на котором выполняется щелчок. Другая идея состоит в том, что нужно сначала добавить все классы-оболочки и затем использовать только метод click для щелчков на классах-оболочках. Тонкость здесь состоит в том, что нужно гарантировать, что все типы окон будут заключены в оболочки специфических элементов управления утилиты Tester.



Применение утилиты Tester


Использовать Tester довольно просто. Следует создать пару Tester-объектов, стартовать или отыскать главное окно приложения, выполнить для него несколько нажатий клавиш, проверить результаты и закончить работу. Листинг 13-1 содержит пример VBScript-теста, который запускает стандартную программу Блокнот (NOTEPAD.EXE), вводит несколько строк текста и закрывает ее.

 Листинг 13-1. Использование общих Tester-объектов 

' Минимальный пример работы с VBScript Tester. В нем просто запускается

' Блокнот, вводится несколько строк текста и Блокнот закрывается.

' Создать объекты системы и ввода.

Dim tSystem

Dim tInput

Dim twin

Set tSystem = «Script.CreateObject ( "Tester.TSystem")

Set tInput = WScript.CreateObject ( "Tester.TInput")

'Запустить Блокнот.

tSystem.Execute "NOTEPAD.EXE"

' Ждать 200 миллисекунд.

tSystem.Pause 200

' Попытка найти главное окно программы Блокнот.

Set twin = tSystem.FindTopWindowByTitle ( "Untitled - Notepad")

If ( twin Is Nothing) Then

MsgBox "Unable to find Notepad!" 

WScript.Quit

 End If

' Убедиться, что Блокнот выполняется в фоновом режиме.

twin.SetForegroundTWindow

' Напечатать (ввести) первую строку.

tlnput.PlayKeys "Be all you can be!~ ~ ~"

' Повторите ввод.

tlnput.PlayKeys "Put on your boots and parachutes....~ ~ ~"

' Ввести третью строку.

tlnput.PlayKeys "Silver wings upon their chests.....~ ~ ~"

' Подождать 1 секунду.

tSystem.Pause 1000

' Закончить Блокнот.

tlnput.PlayKeys "%FX"

tSystem.Pause 50

tlnput.PlayKeys "{TAB}~"

' Сценарий выполнен!

В листинге 13-1 показано три объекта, которые Tester использует наиболее часто. Объект TSystem позволяет находить родительские окна, запускает приложения и приостанавливает тестирование. Объект TWindow, который возвращается функцией FindTopWindowByritie в листинге 13-1, является главной "рабочей лошадкой". Это — оболочка вокруг объекта HWND (дескриптора окна), содержащего полный набор свойств окна.
Дополнительно, TWindow позволяет перечислить все дочерние окна, которые принадлежат конкретному родителю. Последний объект в этом листинге — Tlnput, который поддерживает единственный метод PlayKeys, направляющий клавишные команды окну, имеющему фокус.

В листинге 13-2 показан объект TNotify, используемый в VBScript-тесте. При разработке сценариев автоматизации один из наиболее трудных случаев для обработки — когда неожиданно раскрывается окно, например, панель сообщений от утверждений. Объект TNotify делает его моментальный снимок, обеспечивая тем самым, аварийный обработчик для таких событий. Простой сценарий, приведенный в листинге 13-2, отыскивает окна с заголовком "Блокнот".

 Листинг 13-2. Использование TNotify в VBScript 

' VBScript-тест для отображения обработчиков оконных уведомлений

' Константы для подпрограммы TNotify.AddNotification . Если бы я

' использовал Visual Basic 6, то определил бы здесь константы типа enum .

Const antDestroyWindow = 1

Const antCreateWindow = 2

Const antCreateAndDestroy = 3

Const ansExactMatch = 0

Const ansBeginMatch = 1

Const ansAnyLocMatch = 2

' Создать объекты tSystem и tInput.

Dim tSystem

Dim tInput

Set tSystem = WScript.CreateObject ( "Tester.TSystem")

Set tlnput = WScript.CreateObject ( "Tester.Tlnput")

' Переменная объекта TNotify

Dim Notifier

' Создать объект TNotify.

Set Notifier =

WScript.CreateObject ( "Tester.TNotify" ,' _

"NotepadNotification_" )

' Добавить нужные уведомления. В этой демонстрации используются два

' уведомления — window destroy (ликвидация окна) и

'window create (создание окна). Все возможные комбинации уведомлений 

'см. исходный код TNotify .

Notifier.AddNotification antCreateAndDestroy, _

ansAnyLocMatch , 

_ "Notepad"

' Запуск программы Блокнот.

 tSystem.Execute "NOTEPAD.EXE" 

' Ожидать полсекунды.

 tSystem.Pause 500

' Из-за того что Visual Basic не является потокобезопасным языком,



 ' Because Visual Basic isn't thread-safe

' устанавливаем схему уведомлений, использующую таймер. Однако сообщение

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

 ' в единственном потоке. Эта функция позволяет вручную проверять 

' состояния window create и window destroy.

 Notifier.CheckNotification

' Панель сообщений в процедуре событий NotepadNotification_CreateWindow

 ' блокирована, поэтому код закрытия Блокнота не будет выполняться до тех 

'пор, пока панель сообщений не будет очищена,

 tInput.PlayKeys "%FX"

 tSystem.Pause 50

 tlnput.PlayKeys "{TAB}-" 

' Снова проверить уведомление. 

Notifier.CheckNotification

' Дать TNotify шанс перехватить сообщение о ликвидации окна.

 tSystem.Pause 100

' Отсоединить уведомления. Если вы не сделаете этого в WSH, деструктор

 ' класса никогда не получит вызова, так что уведомление останется

 ' активным в таблице уведомлений. 

WScript.DisconnectObject Notifier 

Set Notifier = Nothing

Sub NotepadNotificationCreateWindow ( twin)

MsgBox ( "Notepad was created!")

 End Sub 

Sub NotepadNotificationDestroyWindow ()

MsgBox ( "Notepad has gone away....")

 End Sub

Время от времени необходимо вызывать метод TNotify checkNotification. (причины изложены чуть позже, в разделе "Реализация утилиты Tester" данной главы). Периодический вызов метода checkNotification гарантирует, что сообщения уведомлений могут проходить, несмотря на то, что в языке, который вы выбрали, может отсутствовать цикл сообщений. Код листинга 13-2 показывает, как нужно использовать панель сообщений в процедурах уведомлений о событиях, хотя применение панели сообщений в реальных сценариях нежелательно, потому что это может вызвать проблемы, неожиданно изменяя окно, обладающее фокусом.

Имейте также в виду, что количество уведомлений ограничено пятью, поэтому нельзя применять TNotify для общих задач сценария, таких как ожидание появления диалогового окна File Save.


TNotify следует использовать только для неожидаемых окон. В зависимости от того, как установлены обработчики уведомлений и как они отыскивают указанный текст в заголовке окна, можно получать уведомления и для таких окон, которые вас, возможно, не интересуют. Наиболее вероятно получение нежелательных уведомлений, когда используется родовая строка, такая как "Блокнот", и указано, что строка может появляться в любом месте заголовка окна. Чтобы избежать нежелательных уведомлений, следует при вызове метода TNotify AddNotification как можно точнее специфицировать уведомления. Процедуры обработки событий CreateWindow также должны просматривать передаваемые им TWindow-объекты, чтобы можно было проверить, что это нужное вам окно. Для процедур обработки событий DestroyWindow, которые обрабатывают родовые уведомления, следует просматривать открытые окна, чтобы гарантировать, что окно, которое вам больше не нужно, не существует.

 Речь, по-видимому, идет об имени основного, родительского окна приложения, способного порождать дочерние окна. — Пер.

Наряду с исходным кодом на сопровождающем компакт-диске вы найдете два других примера использования утилиты Tester. Первый пример, NPAD_TEST.VBS — это более полный VBScript-тест, который включает несколько повторно используемых подпрограмм. Второй — ТТ (или Tester Tester'a) — является главным блочным тестом программы Tester, и вы можете получить доступ к нему с помощью группового файла проекта. TESTER.VBG.TT — это VB-приложение, из которого можно почерпнуть основную идею применения Tester'a в программировании на Visual Basic. Дополнительно, в этих двух примерах показан объект TWindows, который является коллекцией, содержащей объекты TWindow.

Хотя я частично применяю VBScript для блочного тестирования, я понимаю, что правильная его работа весьма сомнительна. VBScript-переменные не типизированы и для VBScript нет редактора, аналогичного редактору Visual Basic, так что при отладке приходится возвращается к старому методу проб и ошибок.


Главная причина привлекательности языка VBScript заключается в том, что не надо компилировать тесты. Если бы вы работали в настраиваемом отладочном окружении, позволяющем легко добавлять двоичные файлы к главному приложению, то рассмотреть и применение языка Visual Basic, благодаря чему можно было бы строить тесты аналогично тому, как вы строите свое приложение. Конечно, утилита Tester не ограничивает разработчика самыми легкими языками, если вам больше нравится язык С или макроассемблер (MASM), то вы вполне можете работать с ними вместо VBScript.

Работа с объектами в Tester'e довольно проста, а серьезное внимание следует уделить планированию тестов, добиваясь их максимальной простоты и целенаправленности. Не пытайтесь возложить на них слишком много работы. Желательно, чтобы каждый сценарий тестировал отдельную операцию. Можно, например, ограничить сценарий посылкой клавиатурной последовательности в открытый файл. Старайтесь обеспечить повторное использование сценариев. Так, сценарий для открытия файла можно применять в трех различных тестах: тесте работоспособности открываемого файла, тесте неработоспособности открываемого файла и тесте поврежденное™ информации в открываемом файле. Как и в нормальной разработке, следует по возможности избегать прямого включения строковых ресурсов в текст программы, а размещать их в файлах ресурсов. Это не только упростит локализацию сценария, но поможет также при изменении системы меню и клавиш быстрого вызова.

Проектируя сценарий Tester'a, полезно задать себе вопрос: "Как проверить, что сценарий выполнен?". Лучшая идея, вероятно, состоит в том, чтобы регистрировать в сценарии состояние приложения в ключевых точках, обеспечивая автоматическое сравнение выводов последовательных прогонов. Если вы используете машину сценариев Windows (WSH-файл CSCRIPT.EXE), то можете, вызвав функцию wscript.Echo, переадресовывать вывод в файл. После того как сценарий закончит работу, можно проанализировать такой файл при помощи утилиты регистрации различий (типа WinDiff); если утилита обнаружит какие-то различия, то можно проверить правильность выполнения сценария.Имейте в виду, что следует нормализовать информацию, отбрасывая специфические детали выполнения. Например, разрабатывая приложение, которое загружает биржевые сводки, не нужно включать в вывод время последнего обновления цен.

Как быть с отладкой сценариев Tester'a? Поскольку Tester не имеет своего собственного интегрированного отладчика, a Visual Basic — имеет, то необходимо проявлять осторожность, чтобы не остановить отладчик на вызове метода Tinput.piayKeys. Если отладчик там остановится, то нажатия клавиш пойдут, очевидно, не к тому окну. Чтобы справиться с этой потенциальной проблемой, перед каждым вызовом piayKeys я устанавливаю окно, к которому посылаю нажатия клавиш, поверх всех остальных окон (вызывая метод TWindow.SetForegroundTWindow). Таким образом, можно прерваться на вызове SetForegroundTwindow, проверить состояние приложения и после этого направлять нажатия клавиш к правильному окну.



Реализация утилиты Tester


Ознакомившись с основными идеями применения Tester'a, рассмотрим некоторые важные моментам его реализации. Сначала для реализации Tester'a я использовал C++ и библиотеку активных шаблонов (Active Template Library — ATL), но затем понял, что намного лучшим выбором был бы Visual Basic. Многое из того, что предполагалось реализовать в Tester'e, не представляло особой сложности, а работу надо было выполнить побыстрее. Поэтому был сделан выбор в пользу Visual Basic, хотя, как вы увидите позже, иногда приходилось прибегать к некоторым уловкам, чтобы заставить его работать.

Первым я начал реализовывать объект класса Tinput, который является ответственным за весь ввод, посылаемый другому окну. Первоначально я думал, что обработка ввода с клавиатуры будет делом простым, — я предполагал строить оболочку вокруг VB-функции SendKeys. Этот подход прекрасно работал, когда коды отдельных клавиш посылались к Блокноту, но когда потребовалось посылать клавиши к Microsoft Outlook 2000, обнаружилось, что некоторые из них до него не доходили. Я никак не мог заставить работать SendKeys и в конце концов был вынужден реализовать собственную функцию, которую назвал PiayKeys. Раздумывая, что же следует включить в эту функцию, я заметил, что Microsoft Windows 98 и 2000 имеют изящную новую функцию — Sendinput. Эта функция также реализована в Windows NT 4, Service Pack 3 (и выше). Функция Sendinput является частью библиотеки Microsoft Active Accessibility (MSAA) и заменяет все предыдущие функции событий низкого уровня, такие как keybd_event. Функция Sendinput обрабатывает клавиатуру, мышь и события аппаратных компонентов. Она также размещает всю информацию ввода от клавиатуры или мыши в специальный входной поток (в виде непрерывного блока), гарантируя тем самым, что этот ввод не будет перемешиваться с любым посторонним пользовательским вводом. Эти функциональные возможности были особенно привлекательны для Tester'a. Быстрое тестирование показало, что функция Sendinput работала также и при посылке ввода в Outlook 2000.


После изучения правил корректной пересылки нажатий клавиш в приложения необходимо было разработать формат клавишного ввода. Поскольку оператор Visual Basic sendKeys обеспечивает удачный формат ввода, я решил просто дублировать его для функции piayKeys и оставил все, кроме кода повторения клавиши. Функция, анализирующая код клавиши, не содержит ничего особенного, — если вы заинтересуетесь этим вопросом, то загляните в каталог SourceCode\Tester\TInputHlp на сопровождающем компакт-диске. Начав работу с объектом Tinput, я все еще намеревался писать Tester на C++ и написал весь код лексического анализа в виде С++-библиотек динамической компоновки (DLL). Метод Tinput. PiayKeys — просто VB-обо-лочка для вызова этой DLL.

Объекты TWindow, Twindows и TSystem довольно просты и должны быть понятны после чтения их исходного кода. Эти три класса реализованы на Visual Basic и являются просто оболочками вокруг некоторых API-функций Windows. Создавая класс TNotify, я столкнулся с некоторыми интересными препятствиями. Задумавшись о том, как он будет определять, было ли окно с конкретным заголовком создано или разрушено, я не предполагал, что создать такой класс будет довольно трудно. Мало того, что работа была нелегкой, но оказалось также, что и уведомления о создании окна не могут быть сделаны "ошибкоустойчивыми" без героических усилий.

Первая идея состояла в том, чтобы реализовать общесистемный обработчик сообщений на основе системы СВ1. Мне показалось, что в SDK-документации говорилось о том, что СВТ-обработчик сообщений является наилучшим методом для определения моментов создания и разрушения окон. Меня подхлестнула быстрая разработка примера, но скоро я наткнулся на препятствие. Когда мой обработчик получал уведомление HCBT_CREATEWND, я не смог согласованно восстановить заголовок окна. После некоторого размышления я предположил, что СВТ-обработчик, вероятно, вызывается как часть обработки сообщения WM_CREATE, и очень немногие окна к этому моменту установили свои заголовки.


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

 СВТ — computer-based training, компьютерное обучение. — Пер

Просмотрев все остальные типы обработчиков, я включил их в свой пример. Как я и подозревал, перехват лишь сообщения WM_CREATE не позволил мне получить заголовок. Один из друзей посоветовал перехватывать сообщения WM_SETTEXT. В конечном счете, чтобы установить заголовок в строке заголовка, почти каждое окно использует сообщение WM_SETTEXT. Конечно, если в приложении создается собственный (неклиентский) рисунок и происходит обмен данными с видеопамятью, то сообщение WM_SETTEXT не годится. Было замечено еще одно интересное обстоятельство: некоторые программы, в частности браузер Microsoft Internet Explorer, много раз последовательно отправляли сообщения WM_SETTEXT с одним и тем же текстом.

Выяснив, что нужно перехватывать сообщения WM_SETTEXT (а не WM_CREATE), я внимательнее рассмотрел различные обработчики, которые можно было использовать. В итоге выбор пал на перехват вызова оконной процедуры WH_CALLWNDPROCRET. Это позволило легко отслеживать оба сообщения — и WM_CREATE, и WM_SETTEXT. Можно также наблюдать сообщения WM_DESTROY. Сначала я ожидал некоторых неприятностей от сообщения WM_DESTROY, т. к. думал, что заголовок окна мог быть освобожден к тому моменту, когда обнаруживается это сообщение. К счастью, заголовок окна имеет силу, пока не получено сообщение WM_NCDESTROY.

Рассмотрев все "за" и "против" обработки сообщений WM_SETTEXT лишь для окон, которые еще не имели заголовка, я решил идти только вперед и обрабатывать все WM_SЕТТЕХТ-сообщения. Альтернативой было бы написание машины состояний (state machine), чтобы сохранять след созданных окон и времени установки их заголовков. Однако казалось, что такое решение чревато ошибками и его трудно реализовать. Препятствием к обработке всех WM_SЕТТЕХT-сообщений было то обстоятельство, что можно получать многократные уведомления о создании для одного и того же окна.


Например, если вы устанавливаете TNotify- обработчик для окон, заголовки которых содержат подстроку "Блокнот", то вы получите уведомление при запуске NOTEPAD.EXE, но вы также получали бы уведомление каждый раз, когда NOTEPAD.EXE открывало бы новый файл. В конце концов я почувствовал, что было бы лучше принять "менее-чем-оптимальную" реализацию, чем тратить много дней на отладку "правильного" решения. К тому же, написание собственно обработчика занимала лишь около четверти полной реализации окончательного TNotify-класса; другие три четверти кода должны были информировать пользователя о создании или разрушении окна.

Я принял решение реализовать Tester на Visual Basic прежде, чем написал класс TNotify. Ранее упоминалось, что использование Tnotify, — не полностью автоматическая операция, и что время от времени необходимо вызывать метод checkNotification. Причина заключается в том, что приложение Visual Basic не может быть многопоточным, а требовалось проверять статус окна, было ли оно создано или разрушено и продолжает ли использовать тот же поток, в котором выполнялась остальная часть TESTER.DLL.

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

 обработчик процедуры WH_CALLWNDPROCRET должен быть общесистемным, поэтому его нужно реализовать в отдельной DLL;  очевидно, что Tester.DLL (т. е. сама утилита Tester) не может быть такой DLL, потому что перенос всей DLL VB-Tester'a и, в свою очередь, MSVBM60.DLL в адресное пространство каждого потока на компьютере пользователя. Это условие означает, что обрабатывающая DLL, вероятно, должна установить флаг или что-то еще, что может прочитать DLL Tester'a, чтобы знать, что условие выполнено; Tester не может быть многопоточным, поэтому вся обработка должна выполняться в одном и том же потоке. Первое уточнение основных требований: функция-обработчик должна быть написана на языке С. Поскольку эта функция загружается во все адресные пространства, сама DLL не может вызывать какие-либо функции из TESTER.DLL, написанной на Visual Basic.


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

Те, кто разрабатывал 16-разрядные Windows-приложения, знают, что поиск способа организации любой фоновой обработки в однопоточной среде без прерываний всегда приводил к API-функции setTimer. С ее помощью можно было организовать фоновую обработку, сохраняя однопоточность приложения. Поэтому я установил уведомление таймера, как часть класса TNotify, чтобы определять, когда были созданы или разрушены окна, которые требовалось контролировать.

Решение в виде процедуры таймера очень похоже на ответ, но, на самом деле, оно только отчасти работает в TNotify. В зависимости от длины сценария и от того, реализует ли выбранный вами язык цикл сообщений, сообщение WM_TIMER может и не проходить, и придется вызвать метод checkNotification, который проверяет также и данные обработчика. Реализуя автоматическую проверку, я пробовал устанавливать метод TSystem. Pause, чтобы вызывать функцию DoEvents по истечении указанного (в TSystem. Pause) интервала времени. К сожалению, применение DoEvents в TSystem.pause перенесло основные проблемы производительности на сценарии, поэтому мне пришлось просто просить пользователей время от времени вызывать метод CheckNotification.

Все эти подробности реализации могут показаться довольно запутанными, но вы удивитесь, увидев, насколько компактным получился Tester. Листинг 13-3 содержит код функции-обработчика из TNOTIFYHLP.CPP. На стороне Tester'a файл TNOTIFY. BAS — это модуль, в котором постоянно хранится процедура таймера, а фактический класс реализован в файле TNOTIFY.CLS. Класс TNotify имеет пару скрытых методов и свойств, к которым модуль TNotify может обращаться, чтобы возбуждать события и определять, какие типы уведомлений хочет получать пользователь. Интересная часть кода подключения — это глобальный совместно используемый сегмент данных (.HOOKDATA), который содержит массив данных уведомлений. При просмотре кода имейте в виду, что данные уведомления глобальны, но вся остальная часть данных находится "внутри процесса".



Листинг 13-3.TNOTIFY.СРР 

/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

"Debugging Applications" (Microsoft Press)

Copyright (c) 1997-2000 John Robbins — All rights reserved.

Главный файл для TNotifyHlp.dll

- - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - * /

#include <tchar.h>

 #include <windows.h> 

#include "TNotifyHlp.h" 

/*///////////////////////////////////////////////////////////////

Определения и константы файловой области видимости

////////////////////////////////////////////////////////////////*/

 // Максимальное количество слотов уведомлений 

static const int TOTAL_NOTIFY_SLOTS = 5; 

// Имя мьютекса

static const LPCTSTR k_MUTEX_NAME = _T ( "TNotifyHlp_Mutex");

 // Максимальное время ожидания на мьютексе 

static const int k_WAITLIMIT = 5000;

// Определение собственной константы TRACE позволяет исключить

 // необходимость переноса BugslayerUtil.DLL в адресное пространство

 // каждого потока.

 #ifdef _DEBUG

#define TRACE ::OutputDebugString

 #else

#define TRACE (void)0

#endif

/*//////////////////////////////////////////////////////////////

Определение типов файловой области видимости

//////////////////////////////////////////////////////////////*/ 

// Структура для поиска индивидуального окна 

typedef struct tagJTNOTIFYITEM 

{

// PID процесса, который создал этот процесс

DWORD dwOwnerPID ;

// Тип уведомления

int iNotifyType;

// Параметр поиска

int iSearchType;

// Дескриптор для создаваемого HWND-объекта

HWND hWndCreate ;

// Булевская переменная

BOOL bDestroy ;

// Строка заголовка

TCHAR szTitle [ МАХ_РАТН ];

 } TNOTIFYITEM, * PTNOTIFYITEM;

 /*///////////////////////////////////////////////////////////////

Глобальные переменные файловой области видимости

////////////////////////////////////////////////////////////////*/ 

// Эти данные НЕ разделяются между процессами, поэтому каждый



 // процесс получает собственную копию.

// HINSTANCE- объект данного модуля. Установка глобальных обработчиков 

// системы требует DLL. 

static HINSTANCE gjnlnst = NULL; 

// Мьютекс, который защищает таблицу g_NotifyData 

static HANDLE g_hMutex = NULL;

// Обработчик перехвата. Этот дескриптор не сохраняется в разделяемой

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

 // при выполнении множественных сценариев.

static HHOOK g_hHook = NULL;

// Количество элементов, добавляемых этим процессом, 

// позволяет определить, как следует обрабатывать подключение,

 static int g_iThisProcess!tems = 0; 

/*/////////////////////////////////////////////////////////////////

Прототипы файловой области видимости

////////////////////////////////////////////////////////////////*/ 

// Наш обработчик

LRESULT CALLBACK CallWndRetProcHook ( int nCode ,

WPARAM wParam, 

LPARAM IParam );

// Внутренняя проверочная функция

static LONG _stdcall CheckNotifyltem ( HANDLE hltem, BOOL bCreate);

 /*//////////////////////////////////////////////////////////////// 

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

/////////////////////////////////////////////////////////////////*/

 #pragma data_seg ( ". HOOKDATA")

 // Таблица элементов уведомлений

static TNOTIFYITEM g_shared_NotifyData [ TOTAL_NOTIFY_SLOTS ] = 



{ 0,0 , 0, NULL, 0, '\0' }, 

{ 0, 0, 0, NULL, 0, '\0' }, 

{ 0, 0, 0, NULL, 0, '\0' },

 { 0, 0, 0, NULL, 0, '\0' }, 

{ 0, 0, 0, NULL, 0, '\0' } 

};

// Главный счетчик

 static int g_shared_iUsedSlots = 0;

 #pragma data_seg ()

/*////////////////////////////////////////////////////

ЗДЕСЬ НАЧИНАЕТСЯ ВНЕШНЯЯ РЕАЛИЗАЦИЯ

///////////////////////////////////////////////////*/ 

extern "C" BOOL WINAPI DllMain ( HINSTANCE hlnst ,

DWORD dwReason , 



LPVOID /*lpReserved*/)

 {

 #ifdef _DEBUG

BOOL bCHRet;

 #endif

BOOL bRet = TRUE;

 switch ( dwReason) 

{

case DLL_PROCESS_ATTACH :

// Установить экземпляр глобального модуля.

g_hlnst = hlnst;

// Нам не нужны поточные уведомления.

DisableThreadLibraryCalls ( g_hlnst);

// Создать мьютекс для этого процесса. Здесь мьютекс создан,

//но еще не присвоен.

g_hMutex = CreateMutex ( NULL, FALSE, k_MUTEX_NAME);

if ( NULL == g_hMutex)

{

TRACE ( _T ( "Unable to create the mutex!\n")); 

// Если нельзя создать мьютекс, то нельзя и 

// продолжать, поэтому отметить сбой в загрузке DLL.

 bRet = FALSE; }

break; 

case DLL_PROCESS_DETACH :

// Проверить, имеет ли этот процесс какие-то элементы в

 // массиве уведомлений. Если имеет, удалить их, чтобы

 // избежать образования потерянных (orphan) элементов.

 if (0 != g_iThisProcess!tems) 

{

DWORD dwProcID = GetCurrentProcessId ();

// Здесь не нужно захватывать мьютекс, потому что

// при наличии сообщения DLL_PROCESS_DETACH всегда будет

// вызываться только одиночный поток.

// Цикл по таблице уведомлений.

for ( int i = 0; i < TOTAL_NOTIFY_SLOTS; i++)

(

if ( g_shared_NotifyData[i].dwOwnerPID == dwProcID) 



#ifdef __DEBUG

TCHAR szBuff[ 50 ] ; 

wsprintf ( szBuff,

_T( "DLL_PROCESS_DETACH removing : #%d\n"),

i);

TRACE ( szBuff);

 #endif

// Избавляемся от сообщения. RemoveNotifyTitle ( (HANDLE)i); 





}

// Закрыть дескриптор мьютекса.

 #ifdef _DEBUG

bCHRet =

 #endif

CloseHandle ( g_hMutex);

 #ifdef _DEBUG

 if ( FALSE == bCHRet) 

{

TRACE ( "!!!!!!!!!!!!!!!!!!!!!!!!\n"); 

TRACE { "CloseHandle(gJiMutex) "

"failed!!!!!!!!!!!!!!!!!!\n"); 

TRACE ( "!!!!!!!!!!!!!!!!!!!!!!!!\n");

 }

#endif

break; 

default :

break; 

}

return ( bRet); 

}

HANDLE TNOTIFYHLP_DLLINTERFACE _stdcall



 AddNotifyTitle ( int iNotifyType, 

int iSearchType, 

LPCTSTR szString )

 {

// Убедитесь, что диапазон типов уведомлений корректен, 

if ( ( iNotifyType < ANTN_DESTROYWINDOW ) || 

( iNotifyType > ANTN_CREATEANDDESTROY ) ) 

{

TRACE ( "AddNotify Title : iNotifyType is out of range!\n"); 

return ( INVALID_HANDLE_VALUE); 

}

// Убедитесь, что диапазон типов поиска корректен,

 if ( ( iSearchType < ANTS_EXACTMATCH ) || 

( iSearchType > ANTS_ANYLOCMATCH) )

{

TRACE ( "AddNotify Title : iSearchType is out of range!\n");

return ( INVALID_HANDLE_VALUE); 

}

// Убедитесь, что строка правильная, 

if ( TRUE == IsBadStringPtr ( szString, MAX_PATH)) 

{

TRACE ( "AddNotify Title : szString is invalid!\n");

return ( INVALID_HANDLE_VALUE); 

}

// Ждать получения мьютекса.

DWORD dwRet = WaitForSingleObject ( g_hMutex, k_WAITLIMIT);

 if ( WAIT_TIMEOUT == dwRet) 

{

TRACE ( _T( "AddNotifyTitle : Wait on mutex timed out!!\n"));

return ( INVALID_HANDLE_VALUE); 

}

// Если все слоты использованы, то — аварийный останов.

 if ( TOTAL_NOTIFY_SLOTS == g_shared_iUsedSlots) 

{

ReleaseMutex ( g_hMutex);

return ( INVALID_HANDLE_VALUE); 

}

// Найти первый свободный слот,

 for ( int i = 0; i < TOTAL_NOTIFY_SLOTS; i++)

{

if ( _T ( '\0') == g_shared_NotifyData[ i ].szTitle[ 0 ])

{

 break;

}

 }

// Добавить эти данные.

g_shared_NotifyData[ i ].dwOwnerPID = GetCurrentProcessId ();

 g_shared_NotifyData[ i ].iNotifyType = iNotifyType; 

g__shared_NotifyData[ i ].iSearchType = iSearchType;

 Istrcpy ( g_shared__Notif yData [ i J.szTitle, szString); 

// Увеличить главный счетчик

. g_shared_iUsedSlots++;

 // Увеличить счетчик этого процесса. 

g_iThis Process Items++;

TRACE ( "AddNotifyTitle - Added a new item!\n"); 

ReleaseMutex ( g_hMutex);



// Если это первый запрос уведомления, активизировать подключение.

 if ( NULL = g_hHook)

 {

g_hHook = SetWindowsHookEx ( WH_CALLWNDPROCRET ,

CallWndRetProcHook, g_hlnst , 0 );

 #ifdef _DEBUG

if ( NULL == g_hHook)

 {

char szBuff[ 50 ];

 wsprintf ( szBuff,

_T ( "SetWindowsHookEx failed!!!! (Ox%08X)\n"), GetLastError ()); 

TRACE ( szBuff); 

}

#endif 

}

return ( (HANDLE)!); 



void TNOTIFYHLP_DLLINTERFACE _stdcall

RemoveNotifyTitle ( HANDLE hltem) 

{

// Проверить значение.

int i = (int)hltem;

if ( ( i < 0) || ( i > TOTAL_NOTIFY_SLOTS))

{

TRACE ( _T ( "RemoveNotifyTitle : Invalid handle!\n"));

  return; 

}

// Получить мьютекс.

DWORD dwRet = WaitForSingleObject ( g_hMutex, k_WAITLIMIT);

 if ( WAIT_TIMEOUT == dwRet) 

{

TRACE ( _T ( "RemoveNotifyTitle : Wait on mutex timed out!\n"));

  return; 

}

if ( 0 = g_shared_iUsedSlots) 

{

TRACE ( _T ( "RemoveNotifyTitle : Attempting to remove when "

"no notification handles are set!\n")); 

ReleaseMutex ( g_hMutex);

  return; 

}

// Перед удалением чего-то, удостоверьтесь, что этот индекс указывает 

// на вход NotifyData, который содержит правильное значение. Без

 // проверки можно вызывать эту функцию с тем же значением

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

if ( 0 == g_shared_NotifyData[ i ].dwOwnerPID) 

{

TRACE ( "RemoveNotifyTitle : Attempting to double remove!\n");

ReleaseMutex ( g_hMutex);

return; 

}

// Удалить этот элемент из массива. 

g_shared_NotifyData[ i ].dwOwnerPID =0; 

g_shared_NotifyData[ i ].iNotifyType = 0;

 g_share.d_NotifyData [ i ] .hWndCreate = NULL;

 g_shared_NotifyData[ i ].bDestroy = FALSE; 

g_shared_NotifyData[ i ].iSearchType = 0;

 g_shared_NotifyData[ i ].szTitle[ 0 ] = _T ( '\0');

 // Декремент главного счетчика элементов.



 g_shared_iUsedSlots- -;

// Декремент счетчика элементов этого процесса.

 g_iThisProcessItems- -;

TRACE ( _Т ( "RemoveNotifyTitle - Removed an item!\n")); 

ReleaseMutex ( g_hMutex);

// Если это последний элемент данного процесса, завершить 

// его обработку.

if ( ( 0 == g_iThisProcess!tems) && ( NULL != g_hHook)) 

{

if ( FALSE = UnhookWindowsHookEx ( g_hHook))

{

TRACE ( _T ( "UnhookWindowsHookEx failed!\n"));

}

g_hHook = NULL; 



} HWND TNOTIFYHLP_DLLINTERFACE _stdcall

CheckNotifyCreateTitle ( HANDLE hltem)

{

return ( (HWND)CheckNotifyltem ( hltem, TRUE));

}

BOOL TNOTIFYHLP_DLLINTERFACE _stdcall

CheckNotifyDestroyTitle ( HANDLE hltem) 

{

return ( (BOOL)CheckNotifyltem ( hltem, FALSE)); 



/*///////////////////////////////////////////////////////////////

ЗДЕСЬ НАЧИНАЕТСЯ ВНУТРЕННЯЯ РЕАЛИЗАЦИЯ

////////////////////////////////////////////////////////////////*/ 

static LONG _stdcall CheckNotifyltem { HANDLE hltem, BOOL bCreate)

{

// Проверить значение.

int i = (int)hltem;

if ( ( i < 0) || ( i > TOTAL_NOTIFY_SLOTS))

{

TRACE ( _T ( "CheckNotifyltem : Invalid handle!\n"));

return ( NULL); 

}

LONG IRet = 0; 

// Получить мьютекс.

DWORD dwRet = WaitForSingleObject ( g_hMutex, k_WAITLIMIT);

 if ( WAIT_TIMEOUT == dwRet) 

{

TRACE ( _T ( "CheckNotifyltem : Wait on mutex timed out!\n"));

return ( NULL);

 }

// Если все слоты пусты, освобождаем мьютекс.

 if ( 0 = g_shared_iUsedSlots) 

{

ReleaseMutex ( g_hMutex);

return ( NULL); 

}

// Проверить затребованный элемент, 

if ( TRUE == bCreate)

 {

// Если HWND-значение не NULL, возвратить это значение

//и обнулить его в таблице.

if ( NULL != g_shared_NotifyData[ i ].hWndCreate)

{

IRet = (LONG)g_shared_NotifyData[ i ].hWndCreate; 

g_shared_NotifyData[ i ].hWndCreate = NULL;

}

 }

else 

{

if ( FALSE != g_shared_NotifyData[ i ].bDestroy)



{

IRet = TRUE;

g_shared_NotifyData[ i ].bDestroy = FALSE;



}

ReleaseMutex ( g_hMutex); 

return ( IRet); 

}

static void _stdcall CheckTableMatch ( int iNotifyType,

HWND hWnd , 

LPCTSTR szTitle ) {

// Захватить мьютекс.

DWORD dwRet = WaitForSingleObject ( g__hMutex, k_WAITLIMIT);

if ( WAIT_TIMEOUT == dwRet)

{

TRACE ( _T ( "CheckTableMatch : Wait on mutex timed out!\n"));

  return; 

}

// Таблица не должна быть пустой, но все надо проверять. 

if ( 0 == g_shared_iUsedSlots) 

{

ReleaseMutex ( g_hMutex);

TRACE { _T ( "CheckTableMatch called on an empty table!\n")); 

return;

 }

// Поиск в таблице.

for ( int i = 0; i < TOTAL_NOTIFY_SLOTS; i++) 

{

// Содержит ли что-нибудь эта запись и согласован ли тип 

// уведомления?

if- ( ( _Т ( '\0') != g_shared_NotifyData[ i ].szTitle[ 0 ]) && 

( g__shared_NotifyData[ i ].iNotifyType & iNotifyType ) ) 

{

BOOL bMatch = FALSE;

// Выполнить согласование.

switch ( g_shared_NotifyData[ i ].iSearchType)

{

case ANTS_EXACTMATCH :

 // Это просто, 

if ( 0 = Istrcmp ( g_shared_NotifyData[i].szTitle,

szTitle )) 

{

bMatch = TRUE; 

}

break;

case ANTS_BEGINMATCH : 

if ( 0 ==

 _tcsnccmp ( g_shared_NotifyData[i].szTitle, 

 szTitle , strlen(g_shared_NotifyData[i].szTitle))) 

{

bMatch = TRUE;

 }

break; case ANTS_ANYLOCMATCH :

if ( NULL != _tcsstr ( szTitle

g_shared_NotifyData[i].szTitle)) 

{

bMatch = TRUE; 

}

break; 

default :

TRACE ( _T ( "CheckTableMatch invalid "\ "search type!!!\n"));

 ReleaseMutex ( g_hMutex);

  return; 

break; 

}

// Согласование выполнено?

 if ( TRUE = bMatch) 

{

// Если это уведомление об уничтожении, проставить "1"

// в таблице.

if ( ANTN_DESTROYWINDOW == iNotifyType)

{

g_shared_NotifyData[ i ].bDestroy = TRUE;



 }

else 

{

// В противном случае, проставить в таблице HWND.

 g_shared_NotifyData[ i ].hWndCreate - hWnd;

 } 





}

ReleaseMutex ( g_hMutex);

 }

LRESULT CALLBACK CallWndRetProcHook ( int nCode ,

WPARAM wParam,

 LPARAM IParam ) 

{

// Буфер для хранения заголовка окна

TCHAR szBuff[ МАХ_РАТН ];

// Всегда передавать сообщение следующему обработчику, прежде чем

// приниматься за какую-нибудь обработку.

LRESULT IRet = CallNextHookEx ( gJnHook, nCode, wParam, IParam);

// The docs say never to mess around with a negative code, so I don't.

// Документация просит никогда не оставлять без внимания

// отрицательные значения, вот я и не оставляю.

if ( nCode < 0) 

{

return ( IRet);

 }

// Получить структуру сообщения. Почему там три (или больше)

 // различных структуры сообщений? Где ошибка в 

// последовательном использовании запаса ole-сообщений для всех 

// обработчиков сообщений/процессов? 

PCWPRETSTRUCT pMsg = (PCWPRETSTRUCT)IParam; 

// Нет заголовка, нет работы

LONG IStyle = GetWindowLong ( pMsg->hwnd, GWL_STYLE); 

if ( WS_CAPTION != ( IStyle & WS_CAPTION)) {

return ( IRet); 

}

// Сообщения WM_DESTROY прекрасно подходят и для диалоговых,

 // и для нормальных окон. Только получите заголовок и проверьте

 // согласование.

if ( WM_DESTROY == pMsg->message) 

{

if (0 != GetWindowText ( pMsg->hwnd, szBuff, MAX_PATH))

{

CheckTableMatch ( ANTN_DESTROYWINDOW, pMsg->hwnd, szBuff);

}

return ( IRet); 

}

// Создание окна сложнее, чем его уничтожение.

 // Получить класс окна. Если это на самом деле диалоговое окно, 

//то нам нужно только сообщение WM_INITDIALOG.

 if ( 0 == GetClassName ( pMsg->hwnd, szBuff, MAX_PATH)) 

{

#iifdef _DEBUG

TCHAR szBuff[ 50 ];

wsprintf ( szBuff ,

 _T ( "GetClassName failed for HWND : 0x%08X\n"), 

pMsg->hwnd ) ;



TRACE ( szBuff); 

#endif

// He так уж много точек, куда можно перейти

return ( IRet); 

}

if ( 0 == Istrcmpi ( szBuff, _T ( "#32770"))) 

{

// Единственное сообщение, которое нужно проверить,

// - ЭТО WM_INITDIALOG.

if ( WM_INITDIALOG == pMsg->message)

{

// Получить заголовок диалогового окна.

if (0 != GetWindowText ( pMsg->hwnd, szBuff, MAX_PATH))

{

CheckTableMatch ( ANTN_CREATEWINDOW,

 pMsg->hwnd ,

 szBuff );

 } 

}

return ( IRet); 

}

//

// Далее речь пойдет о настоящих диалоговых окнах.

 // Решаем, что делать с текущими окнами, 

if ( WM_CREATE == pMsg->message) 

{

// Очень немногие окна устанавливают заголовок в сообщении

// WM_CREATE. Однако некоторые это делают и не используют

// сообщения WM_SETTEXT, поэтому требуется проверка.

if (0 != GetWindowText ( pMsg->hwnd, szBuff, MAX_PATH))

{

CheckTableMatch ( ANTN_CREATEWINDOW, 

pMsg->hwnd , 

szBuff ); 

}

 }

else if ( WM_SETTEXT = pMsg->message)

 {

// Я всегда устанавливаю по умолчанию WM_SETTEXT, потому что эта

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

// приложения, такие как Internet Explorer, вызывают WM_SETTEXT

// многократно'с одним и тем же заголовком. Чтобы не усложнять

// этот обработчик, я просто сообщаю WM_SETTEXT вместо того,

// чтобы поддерживать различные малопонятные и тяжело

// отлаживаемыеструктуры данных, которые сохраняют сведения

// об окнах и предварительно вызывают сообщение WM_SETTEXT.

if ( NULL != pMsg->lParam)

{

CheckTableMatch ( ANTN_CREATEWINDOW , 

pMsg->hwnd , 

(LPCTSTR)pMsg->lParam ); 



}

return ( IRet); 

}

Хотя реализация TNotify была довольно трудноразрешимой задачей, я все-таки остался доволен, что при этом почти не испытал неприятностей. Если собираетесь расширить код обработчика, то уже знаете, что отладка общесистемных обработчиков — непростая задача.


Для ее выполнения вполне годится отладчик Microsoft Visual C++, но я никогда не пытался этого делать. Я использую только отладчик SoftlCE. Существует еще один способ отладки общесистемных подключений. Чтобы воспользоваться им, следует обратиться к так называемому printf-стилю отладки. Применение утилиты DBGVIEW позволяет наблюдать все вызовы функции OutputDebugString и видеть, таким образом, состояние обработчика.

Я столкнулся с одной раздражающей проблемой при разработке Tester'a, которая появлялась только в Windows 98. Весь тест-код прекрасно работал в Windows NT 4 и Windows 2000, но в Windows 98 невозможно было заполнить коллекцию TWindows. Я проверял правильность дескриптора HWND окна, передаваемого в метод Add, с помощью функции iswindow. Беглое чтение документации подсказало, что iswindow возвращает значение типа BOOL. Я ошибочно предположил, что TRUE соответствовало положительным значениям, а FALSE — отрицательным. А в условных конструкциях, я использовал выражение в форме 1 = iswindow (hWndT), которая, очевидно, не работала. Как можно предположить, различные операционные системы не возвращают одни и те же значения. Этот "маленький", но досадный промах весьма поучителен.



Выполнение блочного тестирования интерфейса пользователя



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

Требования, предъявляемые к утилите Tester


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

Вот основные требования к утилите Tester:

1. Ею можно управлять через любой язык, который поддерживает модель компонентных объектов (СОМ).

2. Получив входную последовательность нажатия клавиш, в том же формате, который использует VB-функция sendKeys, Tester должен воспроизвести ее в активном окне.

3. Tester может находить любое высокоуровневое или дочернее окно по его заголовку или классу.

4. По заданному произвольному дескриптору HWND Tester может получить все свойства соответствующего окна.

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

6. Tester должен поддерживать совместное использование сценариев с любым разработчиком команды.

7. Tester допускает расширения своего кода в любом интересующем разработчиков направлении.

Нетрудно заметить, что в этом списке ничего не сказано о действиях с мышью. Зарегистрировать ввод мыши через журнал относительно легко, но, выбрав этот метод, не забудьте об одной маленькой, но важной детали: воспроизведение "мышиных" действий должно выполняться при том же разрешении экрана, при котором они были записаны. Не многие коллективы разработчиков могут соблюдать это требование. Если разрешение экрана при описании действий с мышью задано жестко, то совместное использование сценария становится практически невозможным. Другая проблема состоит в том, что сценарий прерывается, если переместить управление в UI даже на пиксел или два. Сценарий, записанный после того, как UI "замораживается", оказывается слишком хрупким. Фокус в том, что адекватное тестирование невозможно, пока UI "заморожен".

Tester, вероятно, не является подходящим решением для отдела с 20 специалистами по проверке качества (Quality Assurance — QA). Я намеревался создать инструмент, пригодный для автоматизации блочного тестирования в группе, состоящей из нескольких разработчиков. Думаю, что к настоящему моменту Tester удовлетворяет данному требованию. Эта утилита применялась при разработке GUI-отладчика WDBG, рассмотренного в главе 4, и уберегла меня от тысяч нажатий клавиш — так что я могу еще двигать запястьями!