⚙️ Всем привет! Меня зовут Алексей, я 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


Комментарии (0)