UI-автотесты: как правильно организовать код и не сойти с ума

от автора

⚙️ Всем привет! Меня зовут Алексей, я Engineer с 5-летним стажем в автоматизации тестирования. Работаю с различными инструментами автоматизации, включая веб и десктопные решения на C#.
В своей предыдущей статье я рассказывал об автоматизации тестирования с Selenium и C#. Теперь хочу поделиться практикой автоматизации десктопных приложений на примере проекта UIAutomationTestKit.

Содержание

  1. Зачем нужна автоматизация тестирования?

  2. Почему я создал этот проект?

  3. Проблемы десктопной автоматизации

  4. Инструменты и решения

  5. Архитектура проекта

  6. Page Object Pattern vs Controller Pattern

  7. Почему такая архитектура?

  8. Для кого этот проект?

Зачем нужна автоматизация тестирования?

1. Экономия времени и ресурсов

  • Ручное тестирование требует значительных временных затрат, особенно при регрессионном тестировании

  • Автоматизация позволяет запускать сотни тестов за минуты вместо часов ручной работы

  • Параллельное выполнение тестов значительно ускоряет процесс проверки

2. Повышение качества продукта

  • Раннее обнаружение ошибок в процессе разработки

  • Стабильность проверки критических сценариев

  • Повторяемость тестовых сценариев без человеческого фактора

3. Масштабируемость тестирования

[Test] public void Test10_RegistrationSeveralUsers([Values(3)] int number) {     // Тест может быть легко масштабирован для проверки     // любого количества пользователей     for (int i = 0; i < number; i++)     {         _mainWindowController             .SetValidDataInUserForm()             .AssertIsRegistrationButtonEnabled()             .ClickRegistrationButton();     } } 

4. Интеграция в процесс разработки

  • CI/CD пайплайны — автоматический запуск тестов при каждом коммите

  • Непрерывное тестирование — постоянный контроль качества

  • Быстрая обратная связь для разработчиков

5. Документирование функционала

  • Тесты как документация — каждый тест описывает ожидаемое поведение

  • Живые примеры использования функционала

  • Регрессионная защита при рефакторинге

6. Экономическая эффективность

  • Снижение затрат на ручное тестирование

  • Высвобождение ресурсов для более сложных задач

  • Окупаемость в долгосрочной перспективе

7. Улучшение процесса разработки

  • Быстрое обнаружение проблем с архитектурой

  • Уверенность при внесении изменений

💡 Автоматизация тестирования — это не просто инструмент, а стратегическое решение, которое влияет на весь процесс разработки и качество конечного продукта.

Почему я создал этот проект?

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

  • Мало примеров

  • Сложно найти работающие решения

  • Нет готовых шаблонов для организации кода

  • Отсутствие лучших практик

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

Проблемы десктопной автоматизации

1. Сложность локации элементов

// Сложный путь к элементу var element = window.FindFirstDescendant(cf => cf.ByAutomationId("StartButton"))                    .FindFirstChild(cf => cf.ByClassName("Button"))                    .AsButton(); 
  • Разные типы элементов — необходимость работы с WinForms, WPF, UWP

  • Динамические элементы — элементы могут появляться/исчезать

  • Отсутствие уникальных идентификаторов — сложность поиска нужных элементов

2. Нестабильность UI

// Необходимость ожиданий public static bool WaitUntilClickable(this AutomationElement element, int timeoutMs = DefaultTimeout) {     _logger.Info($"Ожидание кликабельности элемента: {element.Properties.AutomationId}");     return Retry.WhileFalse(         () => element?.IsEnabled == true && element?.IsOffscreen == false,         TimeSpan.FromMilliseconds(timeoutMs)).Success; } 
  • Сложная обработка асинхронности — элементы могут появляться с задержкой

  • Проблемы с фокусом — элементы могут быть неактивны или перекрыты

  • Нестабильные идентификаторы — элементы могут менять свои свойства

3. Масштабирование и поддержка

[Test] public void Test10_RegistrationSeveralUsers([Values(3)] int number) {     try     {         for (int i = 0; i < number; i++)         {             _mainWindowController                 .SetValidDataInUserForm()                 .AssertIsRegistrationButtonEnabled()                 .ClickRegistrationButton()                 .Pause(1000);         }     }     catch (Exception exception)     {         _loggerHelper.LogFailedResult(_testName, exception, _reportService);         throw;     } } 
  • Сложность параллельного запуска — тесты могут конфликтовать друг с другом

  • Трудное управление данными — необходимость синхронизации тестовых данных

  • Сложность отладки — трудно понять, что пошло не так при падении теста

4. Отчетность и логирование

private static readonly ILogger _logger = LogManager.GetCurrentClassLogger(); _logger.Info($"Ожидание появления элемента"); 
  • Сложная генерация отчетов — отсутствие встроенных инструментов для создания понятных отчетов

  • Необходимость детального логирования — для анализа проблем и отладки

  • Отсутствие стандартизации — каждый проект решает эти вопросы по-своему

Эти и другие проблемы я постарался решить в своем проекте UIAutomationTestKit, создав гибкую и расширяемую архитектуру.

Инструменты и решения

Почему я выбрал FlaUI?

  1. Поддержка различных UI фреймворков

    // Работа с разными типами элементов var wpfButton = element.AsButton();      // WPF var winFormsButton = element.AsButton(); // WinForms var uwpButton = element.AsButton();      // UWP 
    • Единый API для работы с WinForms, WPF и UWP

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

    • Упрощает поддержку гибридных приложений

  2. Удобный API для работы с элементами

    // Простой поиск элементов var button = window.FindFirstDescendant(cf => cf.ByAutomationId("loginButton"));  // Цепочка методов window.FindFirstDescendant(cf => cf.ByAutomationId("username"))       .AsTextBox()       .Enter("test"); 
    • Интуитивно понятный синтаксис

    • Поддержка цепочек методов

    • Богатый набор методов для работы с элементами

  3. Встроенная поддержка ожиданий

    // Ожидание появления элемента var element = window.WaitUntilClickable(cf => cf.ByAutomationId("button"));  // Ожидание исчезновения элемента window.WaitUntilDisappears(cf => cf.ByAutomationId("loading")); 
    • Готовые методы для работы с асинхронностью

    • Удобные таймауты и повторные попытки

    • Упрощает работу с нестабильными элементами

  4. Активное сообщество

    • Хорошая документация

    • Быстрые ответы на вопросы

    • Открытый исходный код

Почему я выбрал NLog?

  1. Гибкая настройка логов

    <!-- Пример конфигурации NLog --> <targets>   <target name="file" xsi:type="File"           fileName="${basedir}/logs/${shortdate}.log"           layout="${longdate}|${level:uppercase=true}|${logger}|${message}" />   <target name="console" xsi:type="Console"           layout="${time}|${level:uppercase=true}|${message}" /> </targets> 
    • Различные форматы вывода

    • Множество целей для логов

    • Гибкая настройка уровней логирования

  2. Производительность

    // Асинхронное логирование _logger.Info("Начало выполнения операции"); await Task.Run(() => {     // Длительная операция }); _logger.Info("Операция завершена"); 
    • Асинхронное логирование

    • Минимальное влияние на производительность

    • Эффективная работа с большими объемами логов

  3. Богатый функционал

    // Структурированное логирование _logger.Info("Пользователь {username} выполнил вход", username);  // Логирование исключений try {     // Код, который может вызвать исключение } catch (Exception ex) {     _logger.Error(ex, "Ошибка при выполнении операции"); } 
    • Структурированное логирование

    • Поддержка различных форматов

    • Удобная работа с исключениями

  4. Интеграция с другими инструментами

    • Поддержка различных форматов вывода

    • Интеграция с системами мониторинга

    • Возможность отправки логов в различные системы

💡 Выбор FlaUI и NLog позволил мне создать надежную и масштабируемую систему автоматизации, которая легко поддерживается и расширяется.

Архитектура проекта

Мой проект организован следующим образом:

UiAutoTests/ ├── Extensions/      # Расширения для работы с элементами ├── Tests/           # Тестовые сценарии ├── Services/        # Сервисы для работы с приложением ├── Locators/        # Локаторы элементов ├── Helpers/         # Вспомогательные классы ├── Controllers/     # Контроллеры для работы с окнами ├── ControllerAssertions/  # Проверки для контроллеров ├── Clients/         # Клиенты для внешних сервисов ├── Assertions/      # Проверки ├── Core/            # Ядро фреймворка ├── TestCasesData/   # Данные для тестов ├── TestDataJson/    # JSON файлы с тестовыми данными └── NLog.config      # Конфигурация логирования 

Page Object Pattern vs Controller Pattern

Page Object Pattern

public class LoginPage {     private readonly AutomationElement _page;          public LoginPage(AutomationElement page)     {         _page = page;     }          public void EnterUsername(string username)     {         _page.FindFirstDescendant(cf => cf.ByAutomationId("username"))              .AsTextBox()              .Enter(username);     }          public void EnterPassword(string password)     {         _page.FindFirstDescendant(cf => cf.ByAutomationId("password"))              .AsTextBox()              .Enter(password);     }          public void ClickLogin()     {         _page.FindFirstDescendant(cf => cf.ByAutomationId("loginButton"))              .AsButton()              .Click();     } } 

Controller Pattern

public class LoginController {     private readonly AutomationElement _page;          public LoginController(AutomationElement page)     {         _page = page;     }          public LoginController EnterUsername(string username)     {         _page.FindFirstDescendant(cf => cf.ByAutomationId("username"))              .AsTextBox()              .Enter(username);         return this;     }          public LoginController EnterPassword(string password)     {         _page.FindFirstDescendant(cf => cf.ByAutomationId("password"))              .AsTextBox()              .Enter(password);         return this;     }          public LoginController ClickLogin()     {         _page.FindFirstDescendant(cf => cf.ByAutomationId("loginButton"))              .AsButton()              .Click();         return this;     } } 

Преимущества Controller Pattern

  1. Цепочка методов

    // Вместо loginPage.EnterUsername("user"); loginPage.EnterPassword("pass"); loginPage.ClickLogin();  // Можно писать loginController     .EnterUsername("user")     .EnterPassword("pass")     .ClickLogin(); 
  2. Улучшенная читаемость

    // Тест становится более понятным [Test] public void Test_Login() {     new LoginController(_window)         .EnterUsername("user")         .EnterPassword("pass")         .ClickLogin()         .AssertLoginSuccess(); } 
  3. Упрощенное поддержание

    • Меньше дублирования кода

    • Легче добавлять новые методы

    • Проще рефакторить

Почему такая архитектура?

  1. Разделение ответственности

    • Каждый компонент отвечает за свою часть

    • Легко находить и исправлять ошибки

    • Простое добавление нового функционала

  2. Масштабируемость

    // Легко добавлять новые контроллеры public class MainWindowController {     public UserFormController OpenUserForm()     {         // Логика открытия формы         return new UserFormController(_window);     } } 
  3. Поддержка

    • Четкая структура проекта

    • Понятная организация кода

    • Легкое обучение новых членов команды

  4. Тестируемость

    • Изолированные компоненты

    • Простое создание моков

    • Удобное написание тестов

Для кого этот проект?

Начинающие

  • Готовые примеры кода с подробными комментариями

  • Структурированная архитектура

  • Лучшие практики написания качественных автотестов

Опытные разработчики

  • Новые идеи для организации кода

  • Продвинутые техники работы

  • Готовые решения для типичных задач автоматизации

Заключение

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

1. Работа с UI элементами

  • Как мы ищем элементы с помощью FlaUI

  • Наши подходы к ожиданию элементов

  • Работа с разными типами UI элементов

  • Обработка динамических элементов

2. Организация тестов

  • Как устроены наши тестовые сценарии

  • Работа с тестовыми данными

  • Обработка ошибок в тестах

  • Как мы пишем стабильные тесты

3. Логирование

  • Настройка и использование NLog

  • Как мы логируем действия в тестах

  • Структура наших логов

  • Анализ результатов тестов


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

Советы тем, кто начинает

  • Не бойтесь рефакторить — даже если страшно.

  • Смотрите чужие проекты — идеи повсюду.

  • Тестируйте на разных версиях Windows — сюрпризы гарантированы 😅

  • Делайте скриншоты при падении — потом будете смеяться над ошибками.

💡 Мы будем развивать проект и дополнять статьи новыми примерами и лучшими практиками. Подписывайтесь. Мы тут надолго 😉

Полезные ресурсы

  1. UIAutomationTestKit на GitHub

  2. Документация FlaUI

  3. Предыдущая статья про Selenium



ссылка на оригинал статьи https://habr.com/ru/articles/914636/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *