Thursday, July 31, 2014

Введение в Coded UI Tests

Итак, приступим к очередной теме, в которой хочу поделиться с вами новыми знаниями. Сегодня мы рассмотрим использование Code UI Test для покрытия своих приложений, написанных на WPF/Windows Forms. Многим разработчикам знать о том, как пишутся Code UI Test, необязательно. Лично я склоняюсь к тому, что если проект у вас большой, и разработчики пишут к своему коду UI тесты, то в этом проекте что-то неладно с организацией. 
Для разработчика знать, как пишутся UI тесты, является нужным и необходимым, поскольку разработчик начинает писать лучше код и UI интерфейс, так как он побывал в "шкуре" Automation Tester (тестировщика-автоматизатора). Если разработчики не могут понять, как работает процесс, чтобы его автоматизировать, необходимо, чтобы они некоторое время поучаствовали непосредственно в самом процессе. Так они лучше смогут узнать предметную область и говорить с заказчиком на одном языке. Такой подход называется Проблемно-ориентированное проектирование (DDD) (англ. Domain-driven design). Автоматическое тестирование немного не подходит под термин DDD, но оно позволяет посмотреть на предметную область, которая для автоматизаторов является написанным интерфейсом пользователя. 
Существует множество видов фреймворков для тестирования интерфейса пользователя. Как разработчик у прикладных программ, мне интересно тестирование интерфейса пользователя с помощью Microsoft UI Automation API. UI Automation автоматизирует процесс тестирования, по сути, выполняя за нас регрессионное тестирование этого же интерфейса в процессе разработки, поскольку позволяет сразу отловить ошибки, связанные с изменением интерфейса, или его работу, не соответствующую нужным test case.
Мы не будем глубоко закапываться в тестирование с помощью UI Automation, а рассмотрим самый простой вариант создания простого теста. Статью можно назвать следующим образом: "UI Automation for dummies".  
Примечание: чтобы у вас появилась возможность создавать UI тесты для приложения, необходима Visual Studio не ниже Ultimate или Premium, иначе тесты у вас работать не будут.
Для примера я взял свой проект, на котором объяснял работу EventAggregator с библиотеки Prism. Исходники данного примера вы можете взять со статьи "Введение в Prism 5. Использование EventAggregator. Часть первая". В том примере мы имеем вот такое симпатичное окно:
Поскольку моей задачей не было написать автоматические тесты для проекта, то его следует немного модифицировать. Для этого каждый контрол, используемый в данном окне, нужно идентифицировать с помощью свойства AutomationProperties.AutomationId. Оно задает или возвращает строковое значение, которое уникально определяет указанный элемент. Это что-то похожее к тому,  как задавать свойство Name для контрола, но только для Code UI Map. Это уникальное имя будет использовано для того, чтобы построить карту контролов, которые используются в проекте. Эта карта имеет название UI Map. Ниже приведен пример xaml-разметки, которая демонстрирует, что нужно сделать для того, чтобы у нас появилась возможность создание данной UI карты.
<UserControl x:Class="EventAggregatorSample.Views.Regions.ShopOrderView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             >
    <Grid>
        <StackPanel>
            <Label>Customer:</Label>
            <ComboBox AutomationProperties.AutomationId="CustomerCbx"  Name="CustomerCbx" Margin="5"  Width="Auto" HorizontalAlignment="Stretch"
                      ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer}">
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding FirstName}" Width="50"/>
                            <TextBlock Text="{Binding LastName}" />
                        </StackPanel>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
            <Label>Book:</Label>
            <ComboBox AutomationProperties.AutomationId="BooksCbx" Name="BooksCbx" Margin="5"  Width="Auto" HorizontalAlignment="Stretch"
                      ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}">
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel>
                            <TextBlock Text="{Binding Author}" />
                            <TextBlock Text="{Binding Title}" />
                            <TextBlock Text="{Binding Price, StringFormat={}{0:C}}" FontWeight="Bold" />
                        </StackPanel>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
            <Button AutomationProperties.AutomationId="AddButton" Name="AddButton" Margin="5" Width="75" Height="25" HorizontalAlignment="Left"
                    Command="{Binding BuyBookCommand}">Купить</Button>
        </StackPanel>
    </Grid>
</UserControl>
Следующим этапом мы добавим новый проект в наш solution. Для этого на нашем проекте нажимаем правой клавишей и выбираем пункт меню Add -> New Project…
Затем выберем тип проекта Coded UI Test Project и назовем его EventAggregatorCodedUITest по названию исходного проекта.
Затем после нажатия на клавишу ОК у нас появится следующее окно:
Данное окно предлагает нам записать последовательность действий с помощью Code UI Test Builder либо загрузить готовый сценарий. Но поскольку мы тренируемся и это первый наш UI Test, мы выбираем первый вариант, который стоит по умолчанию.
У нас сразу откроется окно Code UI Test Builder, которое вы можете увидеть ниже.
Ниже приведено краткое описание для каждой кнопки.
У меня при создании проекта Code UI Test Project в Visual Studio 2013 наблюдался немного раздражающий дефект, когда сгенерированный код просвечивался красным, словно данных атрибутов или пространств имен не удавалось найти.
Учитывая тот факт, что в 2010 студии такого эффекта у меня не наблюдалось, это немного вводило в ступор, поскольку проект собирался без особых усилий. По умолчанию у нас в проект будет добавлена только одна сборка, хотя должно быть 4.
Ниже показано, сколько сборок должно быть добавлено.
Но эта проблема сразу решается, как только мы сгенерируем самую простую Code UI Map.
Следующем действием перейдем в наш класс CodedUITest1 и найдем следующие закомментированные строки:
//[TestInitialize()]
//public void MyTestInitialize()
//{       
//    // To generate code for this test, select "Generate Code for Coded UI Test" from the shortcut menu and select one of the menu items.
//}
Раскомментируем их и переименуем метод в метод StartApp(), в котором будем запускать наше приложение.
[TestInitialize()]
public void StartApp()
{
           
}
Нам нужно нажать кнопку Start Recording в Code UI Test Builder и запустить наше приложение. После того как мы записали наши действия, нажимаем клавишу Generate Code и назовем наш новый метод StartAppMethod и нажмем кнопку Add and Generate, как показано на рисунке ниже.
После этого сможем увидеть процесс генерации кода.
После этого наш метод StartApp() изменится следующим образом:
[TestInitialize()]
public void StartApp()
{

    this.UIMap.StartAppMethod();

}
Мы также сможем увидеть, что в наш проект добавились следующие файлы:
  • UIMap.uitest – XML-файл, содержащий все элементы управления на UI Map.
  • UIMap.Designer.cs – описание класса UIMap (помечен модификатором partial), содержит кодовое представление файла UIMap.uitest. Автоматически генерируемый файл, что очень важно.
  • UIMap.cs – Файл класса UIMap. Все настройки карты пользовательского интерфейса должны производиться в этом файле.
Если мы перейдем в наш метод StartAppMethod, чтобы увидеть, что у нас сгенерировано, то сможем увидеть следующей код:
public void StartAppMethod()
{
    #region Variable Declarations
    WinEdit uINameEdit = this.UIDebugWindow.UIItemWindow.UIEventAggregatorSamplListItem.UINameEdit;
    WpfComboBox uICustomerCbxComboBox = this.UIMainWindowWindow.UIItemCustom.UICustomerCbxComboBox;
    WpfListItem uIEventAggregatorSamplListItem = this.UIMainWindowWindow.UIItemCustom.UICustomerCbxComboBox.UIEventAggregatorSamplListItem;
    WpfComboBox uIBooksCbxComboBox = this.UIMainWindowWindow.UIItemCustom.UIBooksCbxComboBox;
    WpfListItem uIEventAggregatorSamplListItem1 = this.UIMainWindowWindow.UIItemCustom.UIBooksCbxComboBox.UIEventAggregatorSamplListItem;
    WpfButton uIКупитьButton = this.UIMainWindowWindow.UIItemCustom.UIКупитьButton;
    WpfListItem uIEventAggregatorSamplListItem11 = this.UIMainWindowWindow.UIItemCustom.UICustomerCbxComboBox.UIEventAggregatorSamplListItem1;
    WpfListItem uIEventAggregatorSamplListItem12 = this.UIMainWindowWindow.UIItemCustom.UIBooksCbxComboBox.UIEventAggregatorSamplListItem1;
    #endregion

    // Double-Click 'Name' text box
    Mouse.DoubleClick(uINameEdit, new Point(43, 10));

    // Click 'CustomerCbx' combo box
    Mouse.Click(uICustomerCbxComboBox, new Point(38, 2));

    // Click 'EventAggregatorSample.Models.Customer' list item
    Mouse.Click(uIEventAggregatorSamplListItem, new Point(40, 7));

    // Click 'BooksCbx' combo box
    Mouse.Click(uIBooksCbxComboBox, new Point(43, 4));

    // Click 'EventAggregatorSample.Models.Book' list item
    Mouse.Click(uIEventAggregatorSamplListItem1, new Point(41, 17));

    // Click 'Купить' button
    Mouse.Click(uIКупитьButton, new Point(40, 8));

    // Click 'CustomerCbx' combo box
    Mouse.Click(uICustomerCbxComboBox, new Point(61, 3));

    // Click 'EventAggregatorSample.Models.Customer' list item
    Mouse.Click(uIEventAggregatorSamplListItem11, new Point(48, 12));

    // Click 'BooksCbx' combo box
    Mouse.Click(uIBooksCbxComboBox, new Point(49, 17));

    // Click 'EventAggregatorSample.Models.Book' list item
    Mouse.Click(uIEventAggregatorSamplListItem12, new Point(38, 14));

    // Click 'Купить' button
    Mouse.Click(uIКупитьButton, new Point(27, 15));
}
После этого запустим процесс и сможем увидеть последовательность действий, которые мы совершали. 
Пример:
После выполнения данного теста мы увидим результат успешного прохождения теста на экране.
Если нам нужно написать действие на закрытые окна, пишем аналогично приведенному варианту, только помечаем наш тестовый метод атрибутом TestCleanup. После проделанной работы мы можем открыть нашу UI Map и посмотреть, что у нас получилось (файл UIMap.uitest).
Поскольку мы создали тестовый метод, осталось добавить какую-то проверку в наш пример, чтобы проверить полностью работу UI Automation. Для этого в наш класс CodedUITest1 добавим метод ValidateCustomer().
[TestMethod]
public void ValidateCustomer()
{
           
}
Затем из этого метода (поставив курсор на этот метод) откроем UI Test Builder.
Вот пример, который я добавил с ошибочным значением.
Я добавил проверку, что в списку Customers есть значение "Dunkan Maklaud", но как видно из завалившегося теста я не учел тот факт, что у меня стоит выравнивания текста, поэтому текст упал. Поэтому перейдем на наш метод
[TestMethod]
public void AssertCustomer()
{
    this.UIMap.AssertCustomer();
}
И откроем его в Code UI Test Builder как мы описывали выше. Затем запустим наше приложение EventAggregatorSample и нажмем на кнопку
Затем выберем в качестве цели наш список с клиентами.
Наш контрол будет выделен синей рамкой. В окне Add Assertion мы можем увидеть свойства данного контрола и добавлять нужные нам проверки.
Затем нажмем в этом окне кнопку Add Assertion и добавим новое условие.
Затем повторно выбираем "сгенерировать код" и перезаписываем существующий наш метод.
Запускаем наш тест и видим, что он успешно проходит.
Кстати, проверок у нас немного. Всего девять, которые показаны на рисунке ниже.
Поэтому обычно просто можно проверить, если поменялся текст на UI, пропал контрол с UI Map и т.д.

На этой ноте завершаю введение в автоматическое тестирование. Подобный пример вы можете посмотреть на сайте хабрахабр Time for Coded UI Tests. Надеюсь, теперь тестирование вам будет точно по плечу.