Поделиться через


Тестирование

Автоматизация тестирования пользовательского интерфейса в приложениях WPF

Джеймс Мак-Кэффри (James McCaffrey)

Загружаемый файл с кодом доступен в коллекции кода MSDN
Обзор кода в интерактивном режиме

Содержание

Приложение WPF в процессе тестирования
Автоматизация тестирования пользовательского интерфейса
В заключение

В этой статье я покажу, как написать средство автоматизации тестов пользовательского интерфейса для приложений WPF. Приложения WPF используют новую графическую подсистему, и большинство традиционных методик автоматизации тестирования пользовательского интерфейса с ними просто не работают. К счастью, библиотека Microsoft UI Automation (MUIA) была разработана с учетом потребностей автоматизации пользовательского интерфейса приложений WPF. Библиотеку Microsoft UI Automation можно также использовать для тестирования приложений Win32 и приложений .NET на основе Windows Forms на компьютерах, использующих операционные системы, поддерживающие Microsoft .NET Framework 3.0.

По сравнению с предыдущими альтернативными подходами к автоматизации тестов, библиотека Microsoft UI Automation обладает большими возможностями, более единообразна и, после первоначального ее освоения, куда проще в использовании. Эта статья предполагает, что у читателей нет работы опыта работы с библиотекой MUIA, но есть общее представление о приложениях WPF и средние навыки работы с C#.

Продемонстрировать, о чем я буду вести речь, поможет снимок экрана. На рис. 1 показано, что я тестирую простое, но вполне характерное приложение WPF. Приложение называется CryptoCalc и вычисляет криптографический хэш входной строки, используя одну из трех методик хэширования: хэширование MD5, хэширование SHA1 или хэширование DES.

fig01.gif

Рис. 1. Автоматизация пользовательского интерфейса приложения WPF

Методика хэширования MD5 (Message Digest 5) принимает массив байт произвольного размера и возвращает 16-байтный отпечаток, который затем можно использовать для различных задач идентификации. Методика хэширования SHA1 (Secure Hash Algorithm 1) подобна MD5 за исключением того, что SHA1 использует иной алгоритм и возвращает 20-байтный отпечаток. DES (Digital Encryption Standard) – это методика симметричного шифрования, которую также можно использовать для создания идентифицирующего массива байт. Крипто-хэширование DES возвращает массив байт, который, как минимум, равен по размеру вводимым байтам.

Средство автоматизации тестирования интерфейса пользователя, показанное на рис. 1, работает через консольное приложение, которое запускает тестируемое приложение. Оно использует библиотеку Microsoft UI Automation для получения ссылок на приложение и пользовательские элементы управления на нем и имитирует ввод слова «Hello!», выбор пользователем варианта шифрования DES Encrypt и нажатие элемента управления «кнопка» Compute. Затем средство автоматизации тестирования определяет состояние тестируемого приложения, проверяя текстовое поле, в котором находятся выданные результаты, на наличие ожидаемого значения, после чего сообщает, успешен ли был тест. Я сделал снимок экрана, показанный на рис. 1, непосредственно перед тем, как средство автоматизации тестирования закрыло приложение, имитируя щелчки пользователя элементов меню File, а затем Exit.

В последующих разделах статьи я кратко опишу тестируемое мною приложение WPF CryptoCalc и объясню, как запускать тестируемое приложение, как использовать библиотеку Microsoft UI Automation для получения ссылок на приложение и пользовательские элементы управления и, наконец, как имитировать действия пользователя и проверять состояние приложения. Я также опишу, как можно приспособить и расширить представленную здесь систему тестирования для удовлетворения нужд любого конкретного читателя. Я думаю, что возможность использования новой библиотеки Microsoft UI Automation для тестирования приложений WPF станет отличным дополнением любого набора средств тестирования.

Тестируемое приложение WPF

Для лучшего осознания задач автоматизации тестирования пользовательского интерфейса и проблем разработки, сказывающихся на нем, давайте взглянем на тестируемое приложение WPF. Приложение CryptoCalc – это простое, однооконное пользовательское приложение, вычисляющее криптографический хэш строки. Оно принимает введенную пользователем в элемент управления TextBox строку, длина которой не может превышать 24 символов, преобразует строку ввода в массив байт, вычисляет один из трех типов крипто-хэша введенных байт и отображает получившиеся хэшированные байты во втором элементе управления TextBox.

Я разработал тестируемое приложение, используя Visual Studio 2008 с C#, и назвал проект CryptoCalc. Шаблон WPF создает основу определения пользовательского интерфейса приложения в виде файла XAML:

<Window x:Class="CryptoCalc.Window1"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1" Height="300" Width="300">
  <Grid></Grid>
</Window>

Обратите внимание на то, что элемент управления Window верхнего уровня не содержит атрибута Name. Это важно, поскольку, как мы скоро увидим, при написании средства автоматизации тестов простым способом получения ссылки на элемент управления, используя библиотеку MUIA, является получение доступа к свойству AutomationId, которое создается компилятором из атрибута Name элемента управления. Элементы управления без атрибута Name XAML не смогут получить свойство AutomationId. Это – конкретный, низкоуровневый пример важности учета вопросов надежности, расширяемости и автоматизации тестирования при разработке приложений.

Затем я добавил в приложение элемент управления Label и элемент управления TextBox, перетащив их с панели элементов Visual Studio в рабочую область конструирования:

<Label Height="28" HorizontalAlignment="Left"
 Margin="10,33,0,0" Name="label1" VerticalAlignment="Top"
 Width="120">Enter string:</Label>
<TextBox MaxLength="24" Height="23" Margin="10,57,51,0"
 Name="textBox1" VerticalAlignment="Top" /> 

Следует отметить, что по умолчанию атрибуты Name этих элементов управления получают нормальные значения – label1 и textBox1 соответственно. Затем я разместил три элемента управления RadioButton внутри элемента управления GroupBox. В отличие от старого элемента управления GroupBox в WinForms элемент управления GroupBox в WPF принимает только один элемент, так что я поместил три моих элемента управления RadioButton в единый контейнер StackPanel (который может содержать несколько элементов):

<GroupBox Header="Crypto Type" Margin="10,91,118,127"
 Name="groupBox1">
<StackPanel Height="52" Name="stackPanel1" Width="127">
 <RadioButton Height="16" Name="radioButton1" Width="120">
  MD5 Hash</RadioButton>
 <RadioButton Height="16" Name="radioButton2" Width="120">
  SHA1 Hash</RadioButton>
 <RadioButton Height="16" Name="radioButton3" Width="120">
  DES Encrypt</RadioButton>
</StackPanel>
</GroupBox>

Далее я разместил на форме два элемента: элемент управления Button для запуска вычисления крипто-хэша и элемент управления TextBox для сохранения результатов в основном элементе управления Window:

<Button Height="23" Margin="10,0,0,90" Name="button1"
 VerticalAlignment="Bottom" Click="button1_Click"
 HorizontalAlignment="Left" Width="89">Compute</Button>
<TextBox Height="63" Margin="10,0,51,13" Name="textBox2"
 VerticalAlignment="Bottom" TextWrapping="Wrap" />

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

  <Menu Height="22" Name="menu1"
    VerticalAlignment="Top" IsMainMenu="True" >
  </Menu>

Visual Studio 2008 не поддерживает создание дочерних элементов управления MenuItem с помощью простого перетаскивания, поэтому я вручную дополнил файлу определений XAML элементами меню, добавив элемент меню верхнего уровня File:

  <MenuItem Header="_File">
    <MenuItem Header="_New" Name="fileNew" />
  <MenuItem Header="_Open" Name="fileOpen" />
  <Separator />
  <MenuItem Header="E_xit" Name="fileExit"
    InputGestureText="Alt-F4" ToolTip="Exit CryptoCalc"
    Click="OnFileExit" />
</MenuItem>

WPF поддерживает использование при разработке обычного синтаксиса описания сочетаний клавиш, как, например, _File. Тег <Separator /> ясен и прост. Атрибут ToolTip создаст код, который отображает короткое справочное сообщение при наведении курсора на соответствующий элемент управления MenuItem. При компиляции атрибут Click OnFileExit создаст такой код C#, в котором предполагается существование обработчика ошибок со следующей cигнатурой:

public void OnFileExit(object sender, RoutedEventArgs e) {
  //...       
}

Поэтому после добавления к приложению CryptoCalc кода, реализующего обычную логику, я вручную добавил нужный обработчик события, поместив вызов this.Close в тело метода. Следует отметить, что с элементами управления таких пунктов меню, как New и Open, пока не связано никаких событий. Это сделано специально, чтобы указать на то, что автоматизация тестирования пользовательского интерфейса часто производится в процессе разработки, когда многие компоненты приложения не завершены. Создание элемента управления верхнего уровня Help следует той же схеме, что и для элемента управления File:

<MenuItem Header="_Help">
 <MenuItem Header="Help Topics" />
 <Separator />
 <MenuItem Header="About CryptoCalc" />
</MenuItem>

Контейнер Menu и определения элемента управления MenuItem создадут код на C#, при работе которого, в свою очередь, будет создан интерфейс пользователя, показанный на рис. 2. Завершив создание пользовательского интерфейса, я переключился в окно представления кода. Там я добавил два оператора using к операторам using, созданным Visual Studio в файле Window1.xaml.cs, чтобы иметь возможность доступа к классам и методам шифрования без указания их полных имен:

using System.Security.Cryptography;
using System.IO;

fig02.gif

Рис. 2. Меню файла приложения WPF

Основные функции приложения CryptoCalc содержатся в методе button1_Click. Они перечислены на рис. 3. Сначала текст из элемента управления textBox1 преобразуется в массив байт. Следует отметить, что ради краткости примера я опустил обычную проверку ошибок, которая была бы необходима в рабочей среде.

Рис. 3. Код приложения StatCalc

private void button1_Click(object sender, RoutedEventArgs e)
{
  string input = textBox1.Text;
  byte[] inputBytes = Encoding.UTF8.GetBytes(input);

  if (radioButton1.IsChecked == true) { 
    MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
    byte[] hashedBytes = md5.ComputeHash(inputBytes);
    textBox2.Text = BitConverter.ToString(hashedBytes);
  }
  else if (radioButton2.IsChecked == true) { 
    SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider();
    byte[] hashedBytes = sha.ComputeHash(inputBytes);
    textBox2.Text = BitConverter.ToString(hashedBytes);
  }
  else if (radioButton3.IsChecked == true) { 
    DESCryptoServiceProvider des = new DESCryptoServiceProvider();
    byte[] blanks = System.Text.Encoding.UTF8.GetBytes("        "); // 8 spaces
    des.Key = blanks;
    des.IV = blanks;
    des.Padding = PaddingMode.Zeros;
    MemoryStream ms = new MemoryStream();
    CryptoStream cs =
      new CryptoStream(ms, des.CreateEncryptor(), CryptoStreamMode.Write);
    cs.Write(inputBytes, 0, inputBytes.Length);
    cs.Close();
    byte[] encryptedBytes = ms.ToArray();
    ms.Close();
    textBox2.Text = BitConverter.ToString(encryptedBytes);
  }
} 

Логика приложения разветвляется в зависимости от того, какой из элементов управления RadioButton выбран. Интересно то, что, поскольку свойство IsChecked возвращает тип ?bool (обнуляемое логическое значение), мне нужно явно проверять значение этого свойства на равенство true. Код, необходимый для создания хэшей MD5 и SHA1, должен быть очевиден.

Алгоритм шифрования DES требует 64-битного ключа. Поскольку я использую DES для хэширования, а не для шифрования и дешифрования, то я использую фиктивный ключ, состоящий из восьми символов пробела. При использовании симметричного шифрования для хэширования я обычно использую ключ NULL, состоящий из { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, но объект DESCryptoServiceProvider помечает его как известный ненадежный ключ и вызывает исключение. Я использую тот же массив пробелов для передачи значения так называемому вектору инициализации.

Шифрование DES работает на блоках из 8 байт; поэтому я указываю PaddingMode.Zeros, чтобы весь ввод был дополнен нулями так, чтобы довести его размер до кратного 8 байт. Обратите внимание на то, что PaddingMode.Zeros не будет использоваться для шифрования и дешифрования, поскольку добавление нулей исключает дешифрование (алгоритм дешифрования не может определить, какие нули в тексте шифра являются заполнением, а какие — частью исходного незашифрованного текста).

Автоматизация тестирования пользовательского интерфейса

При написании средств автоматизации тестирования с использованием библиотеки Microsoft UI Automation необходимо знать, как идентифицировать нужные элементы управления в тестируемом приложении. Хороший способ выполнить эту задачу – использование средства UISpy. Для тех, кто о нем не знает: UISpy — это аналог старого средства Spy++ в WPF, которое позволяет изучать свойства компонентов пользовательского интерфейса приложения WPF. Средство UISpy является частью пакета Microsoft Windows SDK и доступно для бесплатной загрузки по адресу microsoft.com/downloads.

На снимке экрана, показанном на рис. 4, видно, что происходит при наведении этого средства на элемент управления Button в приложении CryptoCalc. С точки зрения автоматизации тестирования полями, важными для идентификации элементов управления, являются ControlType (тип элемента управления, в данном случае ControlType.Edit), AutomationId (button1) и Name (имя элемента, в данном случае Compute). При выполнении действий над элементами управления приложения важную роль играет поле списка ControlPatterns (Invoke).

fig04.gif

Рис. 4. Изучение элементов управления с помощью UISpy

Тестирование даже этого крохотного приложения CryptoCalc вручную, через интерфейс пользователя, будет нудным, отнимающим время, приводящим к ошибкам, в общем, неэффективным. Придется вручную занести исходные данные, нажать элемент управления – кнопку Compute, визуально сверить результат с требующимся и записать результат теста: успех или неудача. Гораздо эффективнее написать средство автоматизации тестирования с помощью библиотеки Microsoft UI Automation, чтобы оно имитировало пользователя, работающего с приложением, и определяло бы, верно ли отвечает приложение. Автоматизация однообразных и скучных тестов позволяет высвободить время для тестирования вручную того, что требует опыта и интуиции.

На рис. 5 показана структура тестовой программы, которая выдала результаты, показанные на рис. 1. Я запустил Visual Studio 2008 и создал новое консольное приложение. Здесь я использовал C#, но при желании код моего средства автоматизации тестирования можно легко преобразовать в Visual Basic .NET. Далее я добавил ссылки проекта на библиотеки UIAutomationClient.dll и UIAutomationTypes.dll. Эти библиотеки являются частью .NET Framework 3.0, но проекту Visual Studio по умолчанию они не видны. Обычно они обычно расположены в каталоге C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0. Обратите внимание на то, что библиотека UIAutomationClient.dll содержит ключевые классы, необходимые для автоматизации тестирования. Библиотека UIAutomationTypes.dll содержит различные определения типов, используемые библиотекой автоматизации тестирования MUIA.

Рис. 5. Структура кода средства автоматизации тестирования пользовательского интерфейса

using System;
using System.Diagnostics; 
using System.Threading; 
using System.Windows.Automation; 

namespace Harness {
  class Program {
    static void Main(string[] args) {
      try {
        Console.WriteLine("\nBegin WPF UIAutomation test run\n");
        // launch CryptoCalc application
        // get reference to main Window control
        // get references to user controls
        // manipulate application
        // check resulting state and determine pass/fail
        Console.WriteLine("\nEnd automation\n");
      }
      catch (Exception ex) {
        Console.WriteLine("Fatal: " + ex.Message);
      }
    } // Main()
  } // class
} // ns

Для большего удобства добавлены операторы using, указывающие на пространство имен System.Diagnostics (чтобы было проще использовать класс Process) и пространство имен System.Threading (чтобы было проще использовать метод Thread.Sleep()). Как обычно в средствах автоматизации тестов, я снабжаю мою программу блоком верхнего уровня try/catch для обработки любых неустранимых ошибок. Мой код автоматизации тестирования начинает работу с запуска тестируемого приложения:

Console.WriteLine("Launching CryptoCalc application");
Process p = null;
p = Process.Start("..\\..\\..\\CryptoCalc\\bin\\Debug\\CryptoCalc.exe");

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

int ct = 0;
do {
  Console.WriteLine("Looking for CryptoCalc process. . . ");
  ++ct;
  Thread.Sleep(100);
} while (p == null && ct < 50);

При каждом исполнении цикла программа приостанавливается на 100 миллисекунд. Программа выходит из цикла задержки, если объект процесса не равен null, то есть если процесс был найден или если цикл выполнился 50 раз. На рис. 6 показано, как можно определить, что именно произошло: закончился цикл задержки или был найден процесс AUT.

Рис. 6. Определение того, какое условие выполнилось

if (p == null)
  throw new Exception("Failed to find CryptoCalc process");
else
  Console.WriteLine("Found CryptoCalc process");

// Next I fetch a reference to the host machine's Desktop as an
// AutomationElement object:

Console.WriteLine("\nGetting Desktop");
AutomationElement aeDesktop = null;
aeDesktop = AutomationElement.RootElement;
if (aeDesktop == null)
  throw new Exception("Unable to get Desktop");
else
  Console.WriteLine("Found Desktop\n");

Все элементы управления приложения WPF можно представить как дочерние элементы главного окна приложения. Главное окно приложения является дочерним элементом элемента Desktop, поэтому для получения ссылки на приложение мне нужна ссылка на Desktop. Подключиться к тестируемому приложению можно с помощью метода FindFirst:

AutomationElement aeCryptoCalc = null;
int numWaits = 0;
do {
  Console.WriteLine("Looking for CryptoCalc main window. . . ");
  aeCryptoCalc = aeDesktop.FindFirst(TreeScope.Children,
    new PropertyCondition(AutomationElement.NameProperty, "CryptoCalc"));
  ++numWaits;
  Thread.Sleep(200);
} while (aeCryptoCalc == null && numWaits < 50);

Я использую цикл задержки с развитой логикой вместо Sleep произвольной продолжительности. Метод FindFirst используется, когда искомый элемент управления является одиночным элементом управления. FindFirst принимает два аргумента. Первый – значение области. Тремя наиболее распространенными значениями области являются TreeScope.Children, TreeScope.Descendants и TreeScope.Parent. Поскольку приложение CryptoCalc является непосредственным потомком Desktop, я использую область TreeScope.Children.

Второй аргумент метода FindFirst – это объект, представляющий информацию, которая идентифицирует искомый элемент управления. В нем указывается, что нужно найти такой элемент управления, свойство Name которого имеет значение «CryptoCalc». Можно было использовать и свойство AutomationId, как будет видно чуть ниже. Теперь нужно удостовериться в том, что у меня действительно есть ссылка на главное окно основного приложения:

if (aeCryptoCalc == null)
  throw new Exception("Failed to find CryptoCalc main window");
else
  Console.WriteLine("Found CryptoCalc main window");

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

Console.WriteLine("\nGetting all user controls");
AutomationElement aeButton = null;
aeButton = aeCryptoCalc.FindFirst(TreeScope.Children,
  new PropertyCondition(AutomationElement.NameProperty, "Compute"));
if (aeButton == null)
  throw new Exception("No compute button");
else
  Console.WriteLine("Got Compute button");

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

Затем нужно получить ссылки на два элемента управления TextBox. Хотя два элемента управления TextBox и носят имена textBox1 и textBox2, у них нет свойства Name, поэтому использовать шаблон NameProperty, который использовал для поиска элемента управления Button, нельзя. Тем не менее, у элементов управления TextBox есть свойство AutomationId, которое можно использовать для получения ссылки на элементы управления:

aeTextBox1 = aeCryptoCalc.FindFirst(TreeScope.Children,
  new PropertyCondition(AutomationElement.AutomationIdProperty, "textBox1"));

Вместо этого я решил использовать иной подход, преимущественно в демонстрационных целях. Вместо определения одного элемента управления с помощью свойства имени элемента управления, я использую метод FindAll, чтобы подобрать коллекцию элементов управления по типу. Как выясняется, элементы управления TextBox принадлежат к типу ControlType.Edit, так что код ниже собирает все элементы управления TextBox:

AutomationElementCollection aeAllTextBoxes = null;
aeAllTextBoxes = aeCryptoCalc.FindAll(TreeScope.Children,
  new PropertyCondition(AutomationElement.ControlTypeProperty,
  ControlType.Edit));
if (aeAllTextBoxes == null)
  throw new Exception("No textboxes collection");
else
  Console.WriteLine("Got textboxes collection");

После сбора этой коллекции я могу получить доступ к каждому из TextBox, используя индексацию массивов:

AutomationElement aeTextBox1 = null;
AutomationElement aeTextBox2 = null;
aeTextBox1 = aeAllTextBoxes[0];
aeTextBox2 = aeAllTextBoxes[1];
if (aeTextBox1 == null || aeTextBox2 == null)
  throw new Exception("TextBox1 or TextBox2 not found");
else
  Console.WriteLine("Got TextBox1 and TextBox2");

Как правило, получение ссылок на элемент управления путем использования свойства Name или свойства AutomationId является более оптимальным подходом, чем использование ControlType, но в некоторых ситуациях выбора может не быть. Далее я получаю ссылку на элемент управления RadioButton, который хочу использовать в своем сценарии тестирования:

AutomationElement aeRadioButton3 = null;
aeRadioButton3 = aeCryptoCalc.FindFirst(TreeScope.Descendants,
  new PropertyCondition(AutomationElement.NameProperty,
   "DES Encrypt"));
if (aeRadioButton3 == null)
  throw new Exception("No RadioButton");
else
  Console.WriteLine("Got RadioButton3");

Я использую шаблон, подобный тому, который был использован для получения ссылки на элемент управления Button. Тем не менее, обратите внимание на то, что я указываю TreeScope.Descendants, а не TreeScope.Children. Я делаю это потому, что элемент управления RadioButton является дочерним для элемента управления GroupBox, то есть не непосредственным дочерним элементом главного окна приложения. В качестве альтернативы можно было бы сначала получить ссылку на элемент управления GroupBox (дочерний элемент главного окна приложения), а затем по ней получить ссылку на элемент управления RadioButton. После получения ссылок на элементы управления можно начать управлять тестируемым приложением: Я начну с имитации пользовательского ввода в элемент управления TextBox1:

Console.WriteLine("\nSetting input to 'Hello1!'");
ValuePattern vpTextBox1 =
  (ValuePattern)aeTextBox1.GetCurrentPattern(ValuePattern.Pattern);
vpTextBox1.SetValue("Hello!");

Использование метода SetValue, вероятно, не будет здесь неожиданностью, но обратите внимание: я не произвожу доступ к SetValue() прямо через объект aeTextBox1. Вместо этого я использую промежуточный объект ValuePattern. Концепция объектов AutomationPattern, таких как ValuePattern, является, вероятно, крупнейшим концептуальным камнем преткновения для разработчиков, только начавших знакомство с библиотекой Microsoft UI Automation. Объекты-шаблоны можно представлять себе как абстрактный способ предоставления функциональности элемента управления, который не зависит ни от типа, ни от внешнего вида. Другими словами, экземпляры AutomationPattern, такие как ValuePattern, можно использовать, чтобы включать те функции элемента управления, для которого они созданы.

Для простоты можно представить себе, что ControlType элемента управления предоставляет род, к которому принадлежит элемент управления, а его Pattern предоставляет то, что этот элемент делает. Я использую аналогичный подход для имитации выбора пользователем элемента управления RadioButton3:

Console.WriteLine("Selecting 'DES Encrypt' ");
SelectionItemPattern spSelectRadioButton3 =
  (SelectionItemPattern)aeRadioButton3.GetCurrentPattern(
    SelectionItemPattern.Pattern);
spSelectRadioButton3.Select();

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

Код, который я использую для имитации нажатия кнопки Calculate, должен помочь в объяснении этого:

Console.WriteLine("\nClicking on Compute button");
InvokePattern ipClickButton1 =
  (InvokePattern)aeButton.GetCurrentPattern(
    InvokePattern.Pattern);
ipClickButton1.Invoke();
Thread.Sleep(1500);

Здесь используется InvokePattern, чтобы сделать возможным нажатие кнопки, а затем выполняется само нажатие с помощью метода Invoke. Отметьте задержку в 1,5 секунды, дающую приложению время на ответ. Можно было бы организовать цикл задержки, периодически проверяя, является ли поле результатов textBox2 пустым или нет. На этом этапе тестирования я запустил тестируемое приложение, ввел текст «Hello!» в элемент управления TextBox, отвечающий за ввод, выбрал переключатель DES Encrypt и нажал кнопку Compute.

Теперь нужно проверить элемент управления TextBox2, чтобы увидеть, получено ли то значение, что ожидалось:

Console.WriteLine("\nChecking TextBox2 for '91-1E-84-41-67-4B-FF-8F'");
TextPattern tpTextBox2 =
  (TextPattern)aeTextBox2.GetCurrentPattern(TextPattern.Pattern);
string result = tpTextBox2.DocumentRange.GetText(-1);

Здесь для подготовки к вызову GetText используется TextPattern. Отметьте, что GetText вызывается косвенно, через свойство DocumentRange, которое возвращает текстовый диапазон, заключающий в себя основной текст документа – в данном случае одно текстовое поле. Аргумент -1 метода GetText используется, чтобы убрать ограничение на максимальный размер возвращаемой строки. Другим способом чтения содержимого элемента управления TextBox2 является использование метода GetCurrentPropertyValue:

string result =
  (string)aeTextBox2.GetCurrentPropertyValue(ValuePattern.ValueProperty);

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

if (result == "91-1E-84-41-67-4B-FF-8F") {
  Console.WriteLine("Found it");
  Console.WriteLine("\nTest scenario: Pass");
} 
else {
  Console.WriteLine("Did not find it");
  Console.WriteLine("\nTest scenario: *FAIL*");
}

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

По завершении сценария тестирования я могу закрыть тестируемое приложение, используя элемент управления Menu. Сначала нужно получить элемент управления верхнего уровня File MenuItem:

Console.WriteLine("\nClicking on File-Exit item in 5 seconds");
Thread.Sleep(5000);
AutomationElement aeFile = null;
aeFile = aeCryptoCalc.FindFirst(TreeScope.Descendants,
  new PropertyCondition(AutomationElement.NameProperty, "File"));
if (aeFile == null)
  throw new Exception("Could not find File menu");
else
  Console.WriteLine("Got File menu");

Обратите внимание на то, что я использую область TreeScope.Descendants, поскольку элемент управления File MenuItem является подэлементом контейнерного элемента управления Menu. Затем имитируется щелчок элемента меню File:

Console.WriteLine("Clicking on 'File'");
ExpandCollapsePattern expClickFile =
  (ExpandCollapsePattern)aeFile.GetCurrentPattern(ExpandCollapsePattern.Pattern);
expClickFile.Expand();

Элементы управления MenuItem, у которых есть подменю, предоставляют не шаблон Invoke, как этого можно было бы ожидать; а шаблон Expand. Теперь, когда подменю File отображено и видимо, я могу получить ссылку на команду Exit:

AutomationElement aeFileExit = null;
aeFileExit = aeCryptoCalc.FindFirst(TreeScope.Descendants,
  new PropertyCondition(AutomationElement.NameProperty, "Exit"));
if (aeFileExit == null)
  throw new Exception("Could not find File-Exit");
else
  Console.WriteLine("Got File-Exit");

Ну а теперь я могу использовать InvokePattern на подменю Exit, чтобы закрыть тестируемое приложение CryptoCalc:

InvokePattern ipFileExit =
  (InvokePattern)aeFileExit.GetCurrentPattern(InvokePattern.Pattern);
ipFileExit.Invoke();
Console.WriteLine("\nEnd automation\n");

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

В заключение

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

Шаблон, по которому простой пример, представленный здесь, можно приспособить для тестирования собственных приложений, достаточно прямолинеен. При создании приложений WPF постарайтесь, чтобы все элементы управления имели атрибут XAML Name, что позволит создать AutomationID. Используйте UISpy для определения того, как идентифицировать пользовательские элементы управления и выполнять действия над ними. Определите шаблон MUIA, который позволит проверять состояние и значение пользовательских элементов управления. Располагая этой информацией, можно справиться с большинством ситуаций, типичных при автоматизации тестирования пользовательского интерфейса.

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

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

Доктор Джеймс МакКэффри (James McCaffrey) работает в компании Volt Information Sciences Inc., где он руководит технической подготовкой специалистов по программному обеспечению, работающих в корпорации Майкрософт, Редмонд, университетский городок Вашингтон (Redmond, Washington campus). Он участвовал в разработке нескольких продуктов корпорации Майкрософт, включая Internet Explorer и MSN Search. Джеймс является автором книги «.NET Test Automation Recipes» (издательство Apress, 2006 г.). Написать Джеймсу можно по адресу jmccaffrey@volt.com или v-jammc@microsoft.com.