⚙️ Всем привет! Меня зовут Алексей, я Engineer с 5-летним стажем в автоматизации тестирования. Работаю с различными инструментами автоматизации, включая веб и десктопные решения на C#.
В своей предыдущей статье я рассказывал об автоматизации тестирования с Selenium и C#. Теперь хочу поделиться практикой автоматизации десктопных приложений на примере проекта UIAutomationTestKit.
Содержание
Зачем нужна автоматизация тестирования?
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?
-
Поддержка различных UI фреймворков
// Работа с разными типами элементов var wpfButton = element.AsButton(); // WPF var winFormsButton = element.AsButton(); // WinForms var uwpButton = element.AsButton(); // UWP
Единый API для работы с WinForms, WPF и UWP
Не нужно писать отдельный код для каждого фреймворка
Упрощает поддержку гибридных приложений
-
Удобный API для работы с элементами
// Простой поиск элементов var button = window.FindFirstDescendant(cf => cf.ByAutomationId("loginButton")); // Цепочка методов window.FindFirstDescendant(cf => cf.ByAutomationId("username")) .AsTextBox() .Enter("test");
Интуитивно понятный синтаксис
Поддержка цепочек методов
Богатый набор методов для работы с элементами
-
Встроенная поддержка ожиданий
// Ожидание появления элемента var element = window.WaitUntilClickable(cf => cf.ByAutomationId("button")); // Ожидание исчезновения элемента window.WaitUntilDisappears(cf => cf.ByAutomationId("loading"));
Готовые методы для работы с асинхронностью
Удобные таймауты и повторные попытки
Упрощает работу с нестабильными элементами
-
Активное сообщество
Хорошая документация
Быстрые ответы на вопросы
Открытый исходный код
Почему я выбрал NLog?
-
Гибкая настройка логов
<!-- Пример конфигурации 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>
Различные форматы вывода
Множество целей для логов
Гибкая настройка уровней логирования
-
Производительность
// Асинхронное логирование _logger.Info("Начало выполнения операции"); await Task.Run(() => { // Длительная операция }); _logger.Info("Операция завершена");
Асинхронное логирование
Минимальное влияние на производительность
Эффективная работа с большими объемами логов
-
Богатый функционал
// Структурированное логирование _logger.Info("Пользователь {username} выполнил вход", username); // Логирование исключений try { // Код, который может вызвать исключение } catch (Exception ex) { _logger.Error(ex, "Ошибка при выполнении операции"); }
Структурированное логирование
Поддержка различных форматов
Удобная работа с исключениями
-
Интеграция с другими инструментами
Поддержка различных форматов вывода
Интеграция с системами мониторинга
Возможность отправки логов в различные системы
? Выбор 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
-
Цепочка методов
// Вместо loginPage.EnterUsername("user"); loginPage.EnterPassword("pass"); loginPage.ClickLogin(); // Можно писать loginController .EnterUsername("user") .EnterPassword("pass") .ClickLogin();
-
Улучшенная читаемость
// Тест становится более понятным [Test] public void Test_Login() { new LoginController(_window) .EnterUsername("user") .EnterPassword("pass") .ClickLogin() .AssertLoginSuccess(); }
-
Упрощенное поддержание
Меньше дублирования кода
Легче добавлять новые методы
Проще рефакторить
Почему такая архитектура?
-
Разделение ответственности
Каждый компонент отвечает за свою часть
Легко находить и исправлять ошибки
Простое добавление нового функционала
-
Масштабируемость
// Легко добавлять новые контроллеры public class MainWindowController { public UserFormController OpenUserForm() { // Логика открытия формы return new UserFormController(_window); } }
-
Поддержка
Четкая структура проекта
Понятная организация кода
Легкое обучение новых членов команды
-
Тестируемость
Изолированные компоненты
Простое создание моков
Удобное написание тестов
Для кого этот проект?
Начинающие
Готовые примеры кода с подробными комментариями
Структурированная архитектура
Лучшие практики написания качественных автотестов
Опытные разработчики
Новые идеи для организации кода
Продвинутые техники работы
Готовые решения для типичных задач автоматизации
Заключение
? В этой статье мы рассмотрели основные концепции и структуру проекта. Теперь вы можете самостоятельно изучить код, запустить тесты и попробовать разобраться, как это работает. В следующих статьях мы подробно разберем каждый ключевой компонент:
1. Работа с UI элементами
Как мы ищем элементы с помощью FlaUI
Наши подходы к ожиданию элементов
Работа с разными типами UI элементов
Обработка динамических элементов
2. Организация тестов
Как устроены наши тестовые сценарии
Работа с тестовыми данными
Обработка ошибок в тестах
Как мы пишем стабильные тесты
3. Логирование
Настройка и использование NLog
Как мы логируем действия в тестах
Структура наших логов
Анализ результатов тестов
Мы прошли путь от понимания проблем десктопной автоматизации до создания рабочего решения. Наш проект UIAutomationTestKit - это не просто набор кода, а результат борьбы с реальными проблемами, с которыми сталкивается каждый, кто начинает автоматизировать десктопные приложения.
Советы тем, кто начинает
Не бойтесь рефакторить - даже если страшно.
Смотрите чужие проекты - идеи повсюду.
Тестируйте на разных версиях Windows - сюрпризы гарантированы ?
Делайте скриншоты при падении - потом будете смеяться над ошибками.
? Мы будем развивать проект и дополнять статьи новыми примерами и лучшими практиками. Подписывайтесь. Мы тут надолго ?