Написание автоматических тестов для тестирования пользовательского интерфейса десктопных приложений

    В последние годы тема юнит-тестов, регрессивного тестирования, continuous integration, TDD, BDD, etc становится все популярней и все больше разработчиков начинают активно применять данные техники в своих проектах. При этом отдельным вопросом встает проблема автоматического тестирования пользовательского интерфейса в десктопных приложениях. В этой статье я постараюсь рассмотреть уже существующие решения, а так же привести вариант своего велосипеда написания тестов для UI на .net.

    Постановка задачи


    В начале нужно оговориться, что приведенные ниже методики рассчитаны на применение программистами, желающими автоматизировать процесс проверки созданного ими же UI, так что для команды QA, не владеющей хотя бы базовыми навыками программирования, боюсь, статья окажется мало полезной. Так же замечу, что тесты для UI ни в коем случае не могут считать юнит-тестами, не должны быть включены в цикл написания кода через TDD и желательно должны выполняться на отдельном сервере во время сборки билда(в идеале, конечно, после каждого коммита). Почему не локально? Потому что это будет очень медленно, начнет раздражать и через какое-то время разработчик просто забьет на их запуск.

    Задача для примера у нас будет простая – есть приложение с двумя кнопками. По нажатию первой в текстом поле будет появляться определенный текст(пусть будет “Habrahabr”). По нажатию на вторую туда же будет выводиться текущее время и дата.

    Соответственно, нужно минимум три теста для следующих кейсов:
    1. Изначальный текст в текстовом поле при старте приложения.
    2. Текст после нажатия на первую кнопку.
    3. Текст после нажатия на вторую кнопку.

    Обзор существующих решений


    1. .Net UI Automation


    Сам фреймворк появился довольно давно, вместе с выходом WPF, однако должного освещения в блоггах не нашел. UI Automation представляет из себя библиотеку виртуализации дерева контролов произвольного Win32, Windows Forms или WPF приложения, с возможностью последующего доступа к свойствам этих контролов на чтение и запись. Так же есть поддержка эмуляции эвентов ввода. Если кто-то из читателей в свое время работал с библиотекой Microsoft Active Accessibility, то замечу, что UI Automation является практически прямой ее наследницей.
    В этой библиотеке каждый контрол представляется в виде объекта типа AutomationElement, который предоставляет нам методы по генерации эвентов, получения свойств и поиска дочерних элементов. Самый первый объект AutomationElement для окна нашего приложения приложения можно получить, используя методы AutomationElement.FromHandle(process.MainWindowHandle), где process — ссылка на процесс тестируемого приложения, либо через десктоп:
    AutomationElement.RootElement.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "заголовок окна нашего приложения"));
    * This source code was highlighted with Source Code Highlighter.

    Для специфичных контролов, UI Automation предоставляет набор дополнительных оберток, называемых AutomationPatterns, например ExpandCollapsePattern, SelectionItemPattern и т.д., позволяющий, соответственно, использовать специфичный для этих контролов функционал, например возможность развернуть/свернуть экспандер.

    Достоинства
    • Бесплатность.
    • Поддержка от майкрософт.
    • Является частью .net фреймворка.
    • Возможность тестирования Win32, Windows Forms и WPF приложений.
    Недостатки
    • Необходимость задания пути к исполняемому файла либо завязка на заголовок окна — плохо, т.к. путь может поменяться, а заголовок быть не уникальным.
    • Ограниченный набор доступных свойств — по сути мы можем работать только с теми типами контролов и свойствами, которые майкрософт виртуализировал. Любая кастомизация(например использование сложных составных контролов) приведет к усложнению процесса написания тестов.
    • Так же возможным недостатком может оказаться то, что приложение запускается как сторонний процесс, поэтому, если вдруг возникнет необходимость изменить или подменить поведение какого-то объекта в вашей системе, то начнутся соответствующие танцы на ушах. «А и не надо заменять, тесты должны проверять систему в том же виде, в котором она будет работать у пользователя.» — скажите вы и отчасти будете правы. Вот только когда необходимо проверить поведение системы в критических условиях(отказ на доступ к файлу в определенный момент), убедиться в отсутствие ликов памяти или хотя бы протестировать вывод текущей даты, то откажется, что сделать это, не имея доступа к «внутренностям» системы очень не удобно, долго, а порой и вовсе не возможно.

    Примеры тестов для поставленной задачи

    Изначальный текст в текстовом поле при старте приложения
    1. [TestMethod]
    2. public void TestStartup()
    3. {
    4.   var appPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"..\..\..\TestUI\bin\Debug\TestUI.exe");
    5.   var process = Process.Start(appPath);
    6.   try
    7.   {
    8.     Thread.Sleep(5000);
    9.     var mainWindow = AutomationElement.FromHandle(process.MainWindowHandle);
    10.     var buttonControl = mainWindow.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button));
    11.  
    12.     var textBoxControl = mainWindow.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit));
    13.     var textBox = (ValuePattern)textBoxControl.GetCurrentPattern(ValuePattern.Pattern);
    14.  
    15.     Assert.AreEqual("123123123", textBox.Current.Value);
    16.   }
    17.   finally
    18.   {
    19.     process.Kill();
    20.   }
    21. }
    * This source code was highlighted with Source Code Highlighter.

    Текст после нажатия на первую кнопку.
    1. [TestMethod]
    2. public void TestMethodUIAutomation()
    3. {
    4.   var  appPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),@"..\..\..\TestUI\bin\Debug\TestUI.exe");
    5.   var process = Process.Start(appPath);
    6.   try
    7.   {
    8.     Thread.Sleep(5000);
    9.     var mainWindow = AutomationElement.FromHandle(process.MainWindowHandle);
    10.     var buttonControl = mainWindow.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button));
    11.  
    12.     var textBoxControl = mainWindow.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit));
    13.     var textBox = (ValuePattern) textBoxControl.GetCurrentPattern(ValuePattern.Pattern);
    14.  
    15.     Assert.AreEqual("123123123", textBox.Current.Value);
    16.  
    17.     var button = (InvokePattern) buttonControl.GetCurrentPattern(InvokePattern.Pattern);
    18.     button.Invoke();
    19.  
    20.     Assert.AreEqual("Habrahabr", textBox.Current.Value);
    21.   }
    22.   finally
    23.   {
    24.     process.Kill();  
    25.   }         
    26. }
    * This source code was highlighted with Source Code Highlighter.

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

    2. White project


    Бесплатный фреймворк с кодеплекса, основанный на UI Automation. Достоинства и недостатки те же, отличается только более удобным и расширенным api для работы с деревом контролов.

    Примеры тестов для поставленной задачи

    Изначальный текст в текстовом поле при старте приложения
    1. [TestMethod]
    2. public void TestStartup()
    3. {
    4.   var appPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"..\..\..\TestUI\bin\Debug\TestUI.exe");
    5.   var application = White.Core.Application.Launch(appPath);
    6.  
    7.   Assert.IsNotNull(application);
    8.  
    9.   var window = application.GetWindow("MainWindow");
    10.   var textBox = window.Get<White.Core.UIItems.TextBox>();
    11.  
    12.   Assert.IsNotNull(textBox);
    13.   Assert.AreEqual("123123123", textBox.Text);
    14. }
    * This source code was highlighted with Source Code Highlighter.

    Текст после нажатия на первую кнопку
    1. [TestMethod]
    2. public void TestWithWhite()
    3. {
    4.   var appPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"..\..\..\TestUI\bin\Debug\TestUI.exe");
    5.   var application = White.Core.Application.Launch(appPath);
    6.  
    7.   Assert.IsNotNull(application);
    8.  
    9.   var window = application.GetWindow("MainWindow");
    10.   var textBox = window.Get<White.Core.UIItems.TextBox>();  
    11.  
    12.   var button = window.Get<White.Core.UIItems.Button>(SearchCriteria.ByText("Click for test"));
    13.   button.Click();
    14.  
    15.   Assert.AreEqual("Habrahabr", textBox.Text);
    16. }
    * This source code was highlighted with Source Code Highlighter.

    3. Visual Studio 2010 Coded UI Test


    Coded UI — решение от майкрософт, появившееся в 2010 студии и неоднократно описанное, в том числе и на хабре, например здесь и здесь.

    Достоинства
    • Наличие рекордера, записывающего действия пользователя для автогенерации тестов.
    • Поставка «out of the box» для Visual Studio.
    • Поддержка от майкрософт, интеграция в TFS.
    • Возможность работы на уровне дерева контролов, без привязки к координатам экрана.
    Недостатки

    В целом, набор недостатков тот же, что и у UI Automation. Отдельно только надо выделить то, что возможность работы есть только в определенных версиях 2010 студии(Ultimate, Premium, Professional). Причем если запуск тестов возможен во всех трех, то создание соответствующего типа item'а в проекте и запуск рекордера возможен только в версиях Ultimate и Premium. И если для своих домашних проектов и можно скачать с торрентов купить Ultimate версию, то для коммерческого проекта, где речь идет о лицензии для десятков разработчиков такой шаг может натолкнуться на непонимание со стороны вышестоящего начальства и бухгалтерии.

    Код тестов я приводить не буду, ввиду того, что он является автогенереным и по-этому особого интереса не представляет.

    4. Test Complete и ему подобные


    Системы, подобные Test Complete можно обобщить в одну группы. Я не стану описывать их подробно, т.к. это тема отдельное статьи, выделю лишь некоторые моменты. Основное их достоинство — широкий спектр применений, отсутствие необходимости в навыках программирования и наличие отдельной, не требующих Visual Studio, системы по создание, хранению и поддержки тестов. Недостатки же повторяют предыдущие решения — платность, отношение к тестируемой системе, как к «черному ящику», без возможности мокирования плюс Test Complete имеет собственную среду для запуска тестов, так что использоваться обычный mstest не получится.

    Сделаем что-нибудь свое


    Если вы пишите UI своего приложения на WPF, то для его тестирования можно воспользоваться классом VisualTreeHelper. Алгоритм довольно простой — запускаем в тест-методе наше приложение в отдельном потоке, получаем через VisualTreeHelper нужный контрол, эмулируем эвенты и считываем значения для ассертов.

    Для более удобного создания тестов, для себя я сделал небольшой утилитный класс, упрощающий выполнение рутинных действий:

    Запуск приложения
    var application = UI.Run(() => new App { MainWindow = new MainWindow() });

    * This source code was highlighted with Source Code Highlighter.

    Получения свойства контрола. Поясню, т.к. работать с контролами может только тот тред, в котором их создали, приходиться делать такой финт ушами.
    var window = application.Get(x => x.MainWindow);

    * This source code was highlighted with Source Code Highlighter.

    Ну и поиск в дереве
    var textBox = _mainWindow.FindChild((TextBox el) => el.Name == "SomeText");

    * This source code was highlighted with Source Code Highlighter.

    Еще бывает необходимо проверить лайут, цвета и прочие композиционные вещи. Тогда можно по старинке получить изображение контрола для сравнения через RenderTargetBitmap.
    1. private void AssertRender(string expectImageName, FrameworkElement elementForTest)
    2. {
    3.   var image = elementForTest.Render();
    4.   var expectPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, expectImageName);
    5.   if (!(File.Exists(expectPath) && File.ReadAllBytes(expectPath).SequenceEqual(image)))
    6.   {
    7.     File.WriteAllBytes(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"fail_" + expectImageName), image);
    8.     throw new AssertFailedException(string.Format("Element {0} not equal to image '{1}'", elementForTest.Get(x => x.Name), expectImageName));
    9.   }
    10. }
    11.  
    12. AssertRender("button.png", button);
    * This source code was highlighted with Source Code Highlighter.

    Достоинства
    • Бесплатность.
    • Не требует установки дополнительных библиотек.
    • Позволяет мокировать части системы.
    Недостатки
    • Нет рекордера для тестов. Все пишем руками.
    • Поддержка тестов целиком ложится на ваши плечи.
    Примеры тестов для поставленной задачи

    Изначальный текст в текстовом поле при старте приложения
    1. [TestMethod]
    2. public void TestStartup()
    3. {
    4.   var application = UI.Run(() => new App { MainWindow = new MainWindow() });
    5.   var mainWindow = application.Get(x => x.MainWindow);
    6.  
    7.   var textBox = mainWindow.FindChild((TextBox el) => el.Name == "SomeTexBox");
    8.  
    9.   Assert.IsNotNull(textBox);
    10.  
    11.   Assert.AreEqual("123123123", textBox.Get(x=> x.Text));
    12.  
    13.   application.Invoke(x => x.Shutdown());
    14. }
    * This source code was highlighted with Source Code Highlighter.

    Текст после нажатия на первую кнопку
    1. [TestMethod]
    2. public void TestFirstButtonClick()
    3. {
    4.   var application = UI.Run(() => new App { MainWindow = new MainWindow() });
    5.   var mainWindow = application.Get(x => x.MainWindow);
    6.  
    7.   var textBox = mainWindow.FindChild((TextBox el) => el.Name == "SomeTexBox");
    8.   var button = mainWindow.FindChild((Button el) => el.Content.Equals("Click for test"));
    9.   button.Raise(ButtonBase.ClickEvent);
    10.  
    11.   Assert.AreEqual("Habrahabr", textBox.Get(x => x.Text));
    12.  
    13.   application.Invoke(x => x.Shutdown());
    14. }
    * This source code was highlighted with Source Code Highlighter.

    Текст после нажатия на вторую кнопку.
    1. [TestMethod]
    2. [HostType("Moles")]
    3. public void TestSecondButton()
    4. {
    5.   var application = UI.Run(() => new App { MainWindow = new MainWindow() });
    6.   var mainWindow = application.Get(x => x.MainWindow);
    7.  
    8.   var dateTimeExpect = new DateTime(2011, 12, 08, 12, 30, 25);
    9.   MDateTime.NowGet = () => dateTimeExpect;
    10.  
    11.   var button = mainWindow.FindChild((Button el) => el.Content.Equals("Click for test 2"));
    12.   button.Raise(ButtonBase.ClickEvent);
    13.  
    14.   var textBox = mainWindow.FindChilds<TextBox>().First();
    15.   Assert.AreEqual(dateTimeExpect.ToString(), textBox.Get(x => x.Text));
    16.  
    17.   application.Invoke(x => x.Shutdown());
    18. }
    * This source code was highlighted with Source Code Highlighter.

    Тут я просто замокировал вызов DateTime.Now с помощью фреймворка Moles, обзор которого можно посмотреть, например, здесь.

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

    Заключение


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

    Ссылки


    .Net UI Automation
    White project
    Visual Studio 2010 Coded UI Test
    Test Complete
    Moles
    Исходники тестируемого приложения + примеры тестов с использованием VisualTreeHelper (для запуска теста с датой придется установить Moles)
    • +30
    • 28,4k
    • 7
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 7
    • 0
      Жаль не подходит для тестирования казуальных игрушек.
      • 0
        Почему не подходит? Тот же Coded UI вполне справится с игрушками. Классами Mouse/Keyboard из Microsoft.VisualStudio.TestTools.UITesting можно кликать и печатать. Через ApplicationUnderTest — снимать скриншоты, и по ним делать Assert-ы. Автоматизировать можно тестирование чего угодно, вопрос лишь в окупаемости.
        • 0
          Я тестирую, точнее сертифицирую игрушки методом черного ящика. Стандартный тест-план на 89 тестов. Все попытки отказаться от ручного тестирования пока тщетны…
      • 0
        «десКтопных» приложений, прошу прощения
        • 0
          На данный момент мы не используем автоматизации тестирования гуев, но если такая необходимость встанет, то только Coded UI Test.
          Во первых — не нужно объяснять каждому разработчику, зачем писать полотенце тестов в дополнение к программе (Вы рассмотрели простейший случай, а гуя могут быть достаточно сложным, да как правило так оно и есть).
          Во вторых — это действительно не надо писать. Экономим время.
          В третьих — каждый программист делает 1 ошибку в 10 строках кода и это нормально. Следовательно, если расписать тестирование, то нужно потом это отдебажить. Банальности в виде опечаток и прочего первоитерационного хлама уйдут тут же. А как быть с неявными?
          В четвертых — при вариации тестов — переписывание заново.

          Тестирование должно помогать. Если нужно потратить день, чтобы написать тест к окну, то лучше оттестировать вручную — там и вариативности хоть отбавляй, и тестировщики тоже люди — тыкают куда не попадя и пишут что не попадя, так что есть шанс отловить ошибку ввода данных.
          • 0
            но если такая необходимость встанет, то только Coded UI Test.

            Ну если со стоимостью проблем нет, то выбор действительно неплохой.
            не нужно объяснять каждому разработчику, зачем писать полотенце тестов в дополнение к программе.
            Не очень понял этот момент. А проблем с объяснением зачем писать модульные тесты не возникает?
            вы рассмотрели простейший случай, а гуя могут быть достаточно сложным, да как правило так оно и есть
            Ну простой пример был выбран для удобства демонстрации тестов. В реальной жизни я занимался автоматизацией тестирования UI достаточно сложной системы(что-то типа CADa) и могу сказать, что главное проблемы начинается на тестировании граничных условий. Например вам нужно проверить попап, который возникает при отсутствии свободного места на диске. Как вы будет делать на это автоматический тест силами только Code UI?
            это действительно не надо писать. Экономим время.
            Безусловно, поэтому я и отметил рекорде как плюс.
            каждый программист делает 1 ошибку в 10 строках кода и это нормально.
            Вообще вы как-то утрировали. Ошибки часто делают при описании логики. Делать уровня выполнения при написании 10 ассертов — ну это надо быть талантом. А от опечаток и прочего хорошо защищает статический анализ того же решашрпера. Вообще же речь шла о командах, которые уже используют TDD, пишут регрешен тесты и т.д. Для такие команд написание еще и тестов для UI не стане шоустопером.
            при вариации тестов — переписывание заново.
            Ну изменение продакшен кода конечно может привести к необходимости поменять тесты. Но это абсолютно логично для все автоматического регрешен тестирования вообще. И code ui вас тут не спасет — я удалил часть кнопок, поменять часть контролов, значит надо перезаписать тесты.
            Если нужно потратить день, чтобы написать тест к окну, то лучше оттестировать вручную
            Нет не лучше, т.к. потом после каждой итерации надо будет снова потратить день на ручное тестирования этого окна. Не знаю какой опыт у вас, а я не однократно встречался с ситуацией, когда на одной итерации ломали функционал, сделанный на предыдущих.
            тыкают куда не попадя и пишут что не попадя, так что есть шанс отловить ошибку ввода данных
            Автоматизированное тестирование не противопоставляется ручному, а дополняет его. Чтобы те же тестеры не проходили одни и теже тесткейсы по 100 раз.
            • 0
              Извиняюсь, промахнулся с кнопкой. Еще раз с нормальным форматированием.

              но если такая необходимость встанет, то только Coded UI Test.

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

              Не очень понял этот момент. А проблем с объяснением зачем писать модульные тесты не возникает?
              вы рассмотрели простейший случай, а гуя могут быть достаточно сложным, да как правило так оно и есть

              Ну простой пример был выбран для удобства демонстрации тестов. В реальной жизни я занимался автоматизацией тестирования UI достаточно сложной системы(что-то типа CADa) и могу сказать, что главное проблемы начинается на тестировании граничных условий. Например вам нужно проверить попап, который возникает при отсутствии свободного места на диске. Как вы будет делать на это автоматический тест силами только Code UI?
              это действительно не надо писать. Экономим время.

              Безусловно, поэтому я и отметил рекорде как плюс.
              каждый программист делает 1 ошибку в 10 строках кода и это нормально.

              Вообще вы как-то утрировали. Ошибки часто делают при описании логики. Делать уровня выполнения при написании 10 ассертов — ну это надо быть талантом. А от опечаток и прочего хорошо защищает статический анализ того же решашрпера. Вообще же речь шла о командах, которые уже используют TDD, пишут регрешен тесты и т.д. Для такие команд написание еще и тестов для UI не стане шоустопером.
              при вариации тестов — переписывание заново.

              Ну изменение продакшен кода конечно может привести к необходимости поменять тесты. Но это абсолютно логично для всего автоматического регрешен тестирования вообще. И code ui вас тут не спасет — я удалил часть кнопок, поменять часть контролов, значит надо перезаписать тесты.
              Если нужно потратить день, чтобы написать тест к окну, то лучше оттестировать вручную

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

              Автоматизированное тестирование не противопоставляется ручному, а дополняет его. Чтобы те же тестеры не проходили одни и теже тесткейсы по 100 раз.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.