«Перед каждым релизом уходит целых два дня на то, чтобы вручную прокликать все экраны и всё проверить». «Исправили что-то в прошлом месяце, а рядом сломался другой экран — и обнаружил это уже заказчик». «Веб-команда автоматизировала регрессионное тестирование через Selenium или Playwright, а десктопное приложение так и осталось нетронутым». Такие консультации мы часто слышим от команд, которые давно сопровождают бизнес-приложения на WinForms / WPF.
При этом столь же часто мы слышим и истории о неудачах: команда с энтузиазмом начинает писать тесты для всех экранов, не выдерживает нагрузки на сопровождение и через год всё бросает. Ошибись с выбором инструмента и с тем, «докуда доводить» автоматизацию, — и UI-тестирование обойдётся дороже ручного. И наоборот: если разобраться в механизме, спроектировать тесты так, чтобы они не ломались, и ограничить область смоук-тестами, это превращается в инвестицию, заменяющую «два дня перед релизом» на «двадцать минут ночью без участия человека». В этой статье разберём весь набор приёмов, которые мы используем на реальных проектах: основу — принцип работы UI Automation (UIA), текущее состояние инструментов (FlaUI / WinAppDriver / Appium), реализацию на FlaUI, проектирование устойчивости и безлюдный запуск в CI.
1. Сначала вывод
- Автоматическое UI-тестирование находится на верхнем уровне пирамиды тестирования. Оно медленное, хрупкое, и на диагностику причины сбоя уходит много времени, поэтому оно не заменяет модульные и интеграционные тесты. Логику защищают на нижних уровнях, а UI-тесты ограничивают смоук-проверкой того, что «приложение запускается, и основные сценарии проходят» (глава 6).
- В основе лежит Windows UI Automation (UIA). Элементы ищут по дереву автоматизации, корнем которого выступает рабочий стол, идентифицируют по свойствам вроде AutomationId / Name и управляют через паттерны управления (Invoke / Value / SelectionItem и т. п.). 12
- Как элементы целевого приложения видны на самом деле, стоит проверить ещё до написания кода — через inspect.exe или Accessibility Insights for Windows. inspect — устаревший инструмент, поставляемый вместе с Windows SDK, сейчас официально рекомендуется Accessibility Insights. 3
- Наша рекомендация по инструментам — FlaUI + xUnit / NUnit. FlaUI — это OSS-библиотека под лицензией MIT, тонко оборачивающая UIA, поддерживает и UIA2, и UIA3, и до сих пор активно поддерживается. 4
- WinAppDriver, некогда официальный выбор Microsoft, не получал стабильных релизов с версии v1.2.1 в ноябре 2020 года, а поскольку сам сервер закрытый, сообщество даже не может его чинить. Для новых проектов мы его не рекомендуем (глава 3). 5
- На 80% устойчивость тестов определяется соглашением на стороне разработки о простановке AutomationId. В WPF это
x:NameилиAutomationProperties.AutomationId, в WinForms —Name/AccessibleName. Поиск, полагающийся на отображаемую строку (Name), клики по координатам иThread.Sleepпод запретом — вместо них используют условное ожидание (Retry) и паттерн Page Object (главы 4–5). 67 - Для безлюдного запуска в CI обязательна интерактивная десктопная сессия. Она не работает на агенте, настроенном как служба, и падает при блокировке экрана или разрыве RDP-соединения. Базовая конфигурация — самостоятельно размещённый runner с автоматическим входом (autologon) (глава 7). 8
2. Как это устроено — дерево, свойства и паттерны UI Automation
2.1 Дерево автоматизации
Инструменты автоматического UI-тестирования не распознают экран как изображение. В Windows есть официальная платформа UI Automation (UIA), позволяющая вспомогательным технологиям вроде программ чтения с экрана программно читать и управлять UI приложения, и автоматизация тестирования едет по тем же рельсам. 1
В UIA рабочий стол выступает корнем, а все открытые окна и элементы управления внутри них представлены в виде древовидной структуры. Кнопка, текстовое поле, строка сетки — всё это элементы автоматизации в дереве. Помимо raw-представления, включающего все элементы, дерево имеет и отфильтрованные представления: представление управления (control view), ограниченное только элементами, доступными для операций, и представление содержимого (content view), ограниченное содержимым. 1 Тестовый код в основном обходит представление управления в поисках элементов.
Важно понимать: управлять можно только тем, что опубликовано в дереве, а не тем, что видно глазом. Экраны, собранные из стандартных элементов управления, аккуратно попадают в дерево, а вот списки с собственной отрисовкой (owner-drawn), область чертежа, нарисованная графической библиотекой, или часть стороннего грида могут выглядеть в дереве как «один-единственный элемент». Именно эта видимость и задаёт верхнюю границу того, что способно покрыть автоматическое UI-тестирование. Именно поэтому проверка дерева ещё до написания кода (раздел 2.3) — первый шаг.
2.2 Свойства и паттерны управления
Найти нужный элемент в дереве помогают свойства. На практике используются следующие четыре.
| Свойство | Содержимое | Роль в тестировании |
|---|---|---|
| AutomationId | Идентификатор, назначаемый разработчиком. Не зависит от языка (локали) | Основной ключ поиска. Должен быть уникален среди соседних элементов 6 |
| Name | Имя, производное от отображаемой строки (например, подписи кнопки) | Понятно человеку, но ломается при изменении текста или локализации |
| ControlType | Тип — Button / Edit / ComboBox и т. п. | Помогает сузить поиск |
| ClassName | Имя класса реализации (например, имя класса WinForms) | Последнее средство. Хрупко при изменении реализации |
AutomationId официально определён так: «должен оставаться неизменным вне зависимости от локали» и «должен быть уникален среди соседних элементов» — это свойство, специально предназначенное для того, чтобы автоматические UI-тесты стабильно находили элементы вне зависимости от языка и версии. 6 Иными словами, тестирование UI приложения, где AutomationId не проставлен, вынужденно опирается на хрупкие подсказки вроде отображаемого текста или позиции в дереве. Этому посвящена глава 5.
Управлять найденным элементом позволяют паттерны управления (control patterns). UIA публикует функциональные возможности элемента — «можно кликнуть», «есть значение», «можно выбрать» — как набор паттернов, независимый от типа элемента управления. Сама документация описывает связь между паттернами управления и UI как «аналогичную связи между COM-объектом и его интерфейсами»: у элемента спрашивают, какие паттерны он реализует, и управляют через них. 2 Тем, кто знаком с COM, это можно объяснить как UI-версию QueryInterface. Основные паттерны — в таблице.
| Паттерн | Что позволяет | Типичные элементы |
|---|---|---|
| Invoke | Выполнение действия по умолчанию (аналог клика) | Кнопки, пункты меню |
| Value | Получение и установка значения | Текстовые поля |
| SelectionItem / Selection | Выбор элемента, получение состояния выбора | Списки, выпадающие списки, вкладки |
| Toggle | Переключение вкл/выкл | Флажки |
| ExpandCollapse | Разворачивание/сворачивание | Выпадающие списки, узлы дерева |
| Text | Чтение текстового содержимого | Документы, форматированный текст |
| Window | Разворачивание/сворачивание/закрытие | Окна верхнего уровня |
| Scroll / ScrollItem | Прокрутка, перевод элемента в видимую область | Списки, сетки |
Тестовый код, «кликающий по кнопке», под капотом делает следующее: «получает паттерн Invoke этого элемента и вызывает Invoke()». Он не вычисляет координаты и не отправляет события мыши, а вызывает операцию, которую публикует сам элемент управления, — именно эта разница и делает тесты стабильными вне зависимости от положения окна и DPI.
2.3 Проверяем «видимое» через inspect.exe и Accessibility Insights
Лучший способ узнать, с какими AutomationId / Name / ControlType / паттернами опубликованы элементы целевого приложения, — посмотреть на них через инструмент.
- inspect.exe: классический инструмент, поставляемый вместе с Windows SDK (находится в
bin\<version>\<platform>каталога установки SDK). Выбрав элемент мышью или фокусом клавиатуры, вы получаете список UIA-свойств и паттернов, а также можете проверить навигацию по дереву. Однако официально он позиционируется как устаревший инструмент, и рекомендуется переход на Accessibility Insights. 3 - Accessibility Insights for Windows: текущий рекомендуемый инструмент Microsoft. Удобна функция Live Inspect, позволяющая посмотреть UIA-свойства элемента простым наведением мыши или переводом фокуса, есть и автоматическая проверка с точки зрения доступности (FastPass). 3
- FlaUInspect: инспектор, поставляемый вместе с проектом FlaUI. Позволяет посмотреть дерево с точки зрения UIA2 и UIA3 — тех реализаций, которые реально использует FlaUI, поэтому его стоит установить, если вы собираетесь писать тесты именно на FlaUI. 4
По опыту, первое, что стоит сделать при внедрении автоматического UI-тестирования, — не «написать тестовый код», а открыть основные экраны в инструменте типа inspect и провести инвентаризацию того, насколько плотно проставлен AutomationId. Если тут пусто, в итоге быстрее оказывается начать с доработки самого приложения (глава 5).
3. Выбор инструмента — почему FlaUI и в каком состоянии WinAppDriver
Обращаться к UIA можно и напрямую через COM, но на практике используют библиотеку-обёртку. Разберём варианты по состоянию на 2026 год, опираясь на проверенные факты.
| Инструмент | Форма | Текущее состояние (на 2026 год) | Ориентир для новых проектов |
|---|---|---|---|
| FlaUI | .NET-библиотека (MIT) | Активно поддерживаемый OSS. v5.0.0 вышла в феврале 2025 4 | Первый выбор |
| WinAppDriver | Сервер по протоколу WebDriver (Microsoft) | Последняя стабильная версия v1.2.1 — ноябрь 2020. v1.3 так и осталась RC от июля 2020. Более 1100 нерешённых issue 5 | Фактически заморожен. Избегать для новых проектов |
| Appium Windows Driver | Драйвер Appium для Windows | Внутри использует WinAppDriver, поэтому наследует те же ограничения 9 | Только при наличии активов на Appium |
| Coded UI Tests | Функция Visual Studio | Устарела в VS 2019, удалена в VS 2026 10 | Объект миграции |
3.1 FlaUI — сейчас самая практичная тонкая обёртка над UIA
FlaUI — .NET-библиотека, поддерживающая автоматическое UI-тестирование Windows-приложений (Win32 / WinForms / WPF / приложения из Store), спроектированная как обёртка над нативными библиотеками UIA от Microsoft. 4 Пакеты разделены на общую часть FlaUI.Core и FlaUI.UIA2 / FlaUI.UIA3 — в зависимости от того, какую реализацию UIA вы используете.
Выбор между UIA2 и UIA3 явно описан в FAQ FlaUI. UIA2 — только управляемая реализация, не поддерживает новые возможности вроде touch и хуже работает с WPF и приложениями из Store; UIA3 — более новая реализация, оптимальна для WPF/Store, но в приложениях WinForms может натыкаться на баги, которых нет в UIA2. 4 Иными словами, практический ориентир — пробовать UIA3 для WPF и UIA2 для WinForms и использовать то, что окажется стабильнее на вашем конкретном приложении. Возможность подключить оба варианта из NuGet и переключаться между ними — приятная сторона того, что FlaUI остаётся тонкой обёрткой.
Собственного тестового фреймворка у FlaUI нет, поэтому её комбинируют с xUnit / NUnit / MSTest и пишут как обычный тестовый проект. То, что тесты запускаются тем же test runner-ом и в том же CI-конвейере, что и модульные тесты, заметно облегчает эксплуатацию.
3.2 WinAppDriver — если честно, фактически заморожен
WinAppDriver — тестовый сервер от Microsoft, позволяющий управлять Windows-приложениями по тому же протоколу WebDriver, что и Selenium, и одно время считался основным вариантом. Когда Coded UI Tests в Visual Studio были признаны устаревшими, сама Microsoft рекомендовала в качестве пути миграции: «для Web — Selenium, для десктопа и UWP — Appium + WinAppDriver». 10
Однако, если посмотреть текущее состояние на GitHub, последний стабильный релиз v1.2.1 вышел в ноябре 2020 года, и с тех пор стабильных версий не было. v1.3 так и остаётся Release Candidate (v1.2.99) от июля 2020 года, официального релиза не случилось, а число нерешённых issue превышает 1100. 5 Ещё более неприятно то, что в GitHub-репозитории есть только документация, примеры и трекер issue — исходный код самого сервера не опубликован. Это означает, что сообщество даже не может исправлять баги. Наша оценка: «раз это официальный продукт, значит безопасно» — недостаточное основание для того, чтобы брать его в 2026 году для нового проекта. Если у вас уже есть работающие тестовые активы на WinAppDriver, немедленно от них избавляться не нужно, но наращивать их не стоит — новые сценарии тестирования лучше переводить на FlaUI.
3.3 Appium Windows Driver и другие варианты
Appium Windows Driver (appium-windows-driver) — драйвер, позволяющий управлять Windows-приложениями из Appium, но по сути это «интерфейс к WinAppDriver, предоставляемому Microsoft», и всю тяжёлую работу выполняет именно WinAppDriver. 9 Поэтому он полностью наследует застой WinAppDriver. Он по-прежнему остаётся вариантом для команд, унифицировавших тестирование мобильных приложений и Web через Appium, или для тех, кто хочет писать тестовый код на Java или Python, — но и в этом случае стоит принимать решение, понимая ограничения и не самое радужное будущее, унаследованные от WinAppDriver.
Приложения на WinUI 3 (Windows App SDK) тоже поддерживают UIA, поэтому их можно тестировать через FlaUI (UIA3). Правда, накопленного опыта здесь меньше, чем для WinForms/WPF, поэтому предварительная проверка через инструменты типа inspect становится ещё важнее — из-за особенностей отображения отдельных элементов управления. Выбор самого UI-фреймворка разобран в статье «Как выбрать между WinForms, WPF и WinUI — практическая таблица решений».
4. Минимальная реализация на FlaUI — запуск, поиск, операции, проверка
Хватит теории — посмотрим на рабочий код. Это обычный тестовый проект с FlaUI.UIA3 (или FlaUI.UIA2, если для WinForms она окажется нестабильной) и xUnit, подключёнными через NuGet.
4.1 Базовая форма смоук-теста
Тест сценария «запустить приложение, открыть диалог регистрации заказа, сохранить одну запись и убедиться, что результат появился в статусе» можно написать так.
using FlaUI.Core;
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Tools;
using FlaUI.UIA3;
using Xunit;
public class OrderSmokeTest
{
[Fact]
public void OrderEntry_MainFlowWorks()
{
using var app = Application.Launch(@"C:\App\OrderManager.exe");
using var automation = new UIA3Automation();
try
{
// Ждёт появления главного окна
var window = app.GetMainWindow(automation);
// Находим элемент по AutomationId и управляем им как Button
window.FindFirstDescendant(cf => cf.ByAutomationId("NewOrderButton"))
?.AsButton().Invoke();
// Диалог открывается асинхронно, поэтому ждём условно, пока он появится
var dialog = Retry.WhileNull(
() => window.FindFirstDescendant(
cf => cf.ByAutomationId("OrderDialog"))?.AsWindow(),
timeout: TimeSpan.FromSeconds(5)).Result;
Assert.NotNull(dialog);
// Вводим текст через паттерн Value (не эмуляция клавиатуры)
dialog.FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox"))
.AsTextBox().Text = "Тест Трейдинг ООО";
dialog.FindFirstDescendant(cf => cf.ByAutomationId("SaveButton"))
.AsButton().Invoke();
// Завершение сохранения тоже проверяем условным ожиданием по метке статуса
var saved = Retry.WhileFalse(
() => window.FindFirstDescendant(
cf => cf.ByAutomationId("StatusLabel"))
?.Name.Contains("Сохранено") == true,
timeout: TimeSpan.FromSeconds(10));
Assert.True(saved.Success);
}
finally
{
app.Close();
}
}
}
Здесь всего четыре составляющих.
- Запуск:
Application.Launchзапускает процесс (к уже запущенному приложению можно подключиться черезApplication.Attach).GetMainWindowждёт, пока станет доступно главное окно. - Поиск:
FindFirstDescendant(cf => cf.ByAutomationId(...))— это поиск по дереву.cf— фабрика условий, можно составлять и условияByName/ByControlType/And. Как уже говорилось, основной ключ поиска — AutomationId. - Операция: найденный элемент преобразуют в типизированную обёртку
AsButton()/AsTextBox()/AsComboBox()и т. п. и управляют им.Invoke()— это паттерн Invoke, свойствоText— паттерн Value: паттерны управления из раздела 2.2 стоят прямо за этими вызовами. - Проверка: делаем assert результатов, появляющихся в UI (метки, число строк списка, заголовок окна и т. п.). Если нужно проверить и содержимое БД, ничто не мешает читать её прямо из тестового кода.
4.2 Ожидание через Retry — если пишете Sleep, вы уже проиграли
Главный источник нестабильности (flaky test) UI-тестов — это тайминг. Пока не откроется диалог, пока не загрузятся данные, пока не станет активной кнопка — UI постоянно меняется асинхронно, и если тестовый код исходит из предположения «оно уже наверняка отображается», получается тест, который падает только на медленных машинах.
Но это не значит, что вставить Thread.Sleep(3000) — хорошее решение, это худший вариант. На быстрой машине это впустую тратит время, на медленной — времени не хватает, и тест всё равно падает, а суммарное время выполнения всех тестов только растёт. Ответ — условное ожидание: опрашивать до выполнения условия и прерываться по тайм-ауту, и в FlaUI для этого есть специальный класс Retry. 4
// Ждём до 5 секунд, пока значение перестанет быть null (то есть элемент появится)
var element = Retry.WhileNull(
() => window.FindFirstDescendant(cf => cf.ByAutomationId("ResultGrid")),
timeout: TimeSpan.FromSeconds(5),
interval: TimeSpan.FromMilliseconds(200),
throwOnTimeout: true).Result;
// Ждём, пока условие не станет true (то есть кнопка не станет активной)
Retry.WhileFalse(
() => saveButton.IsEnabled,
timeout: TimeSpan.FromSeconds(5),
throwOnTimeout: true);
Есть готовые Retry.WhileNull / WhileFalse / WhileTrue / WhileException и подобные, с возможностью задать тайм-аут, интервал опроса и то, бросать ли исключение по тайм-ауту. 4 Стоит отметить: начиная с версии 2.0 FlaUI отказалась от неявных повторов в методах поиска Find, приняв политику «где ждать — явно указывает тестовый код». 4 FindFirstDescendant видит только «дерево в этот самый момент», поэтому закрепите правилом: поиск асинхронно появляющегося элемента всегда оборачивать в Retry. Сама идея «строить явное условие ожидания вместо того, чтобы прикрывать тайминг через Sleep» — общее правило для программирования под Windows в целом, не только для UI-тестирования (см. «Почему в Windows стоит предпочитать ожидание события Sleep(1)»).
5. Проектирование устойчивости — соглашения на стороне приложения и тестов
Причины, по которым UI-тесты «бросают из-за непосильного сопровождения», фактически сводятся к четырём: поиск, зависящий от отображаемого текста, клики по координатам, Sleep и нехватка общей структуры. Разберёмся с каждой через соглашение.
5.1 Обязательно проставлять AutomationId на стороне разработки
Это самое важное. Устойчивость теста определяется не столько способом написания тестового кода, сколько тем, публикует ли приложение стабильные идентификаторы. AutomationId — именно то свойство, которое для этого предназначено: по спецификации оно должно быть независимым от локали и уникальным среди соседних элементов. 6
В WPF элемент с заданным x:Name использует это имя как идентификатор на стороне UIA, поэтому уже именованные элементы управления тестируемы без дополнительной работы. Если нужно назначить идентификатор явно — например, внутри шаблона данных, — устанавливают присоединённое свойство AutomationProperties.AutomationId. 67
<!-- x:Name становится идентификатором как есть -->
<Button x:Name="SaveButton" Content="Сохранить" Click="OnSave" />
<!-- Внутри шаблонов и подобных мест явно задаём AutomationProperties.AutomationId -->
<Button AutomationProperties.AutomationId="DeleteRowButton"
Content="Удалить"
Command="{Binding DeleteCommand}" />
В WinForms для идентификации на стороне UIA используется Control.Name, задаваемое в дизайнере (то самое имя, которое меняют с button1 на saveButton). По нашему опыту, формы, где Name проставлено по соглашению, почти всегда можно искать по AutomationId как есть, но поскольку отображение отличается в зависимости от поколения фреймворка и типа элемента, обязательно проверяйте реальный AutomationId через инструмент типа inspect, прежде чем использовать его как ключ поиска в тесте. Кроме того, свойство Name в UIA, которое становится озвучиваемым именем для программ чтения с экрана, для многих элементов управления берётся из свойства Text, но для типов вроде TextBox или ListView, где это не происходит автоматически, требуется явно задать AccessibleName. 11 То, что настройка AutomationId нужна не только для тестов, а представляет собой ровно ту же работу, что и поддержка доступности, — хороший аргумент при обосновании бюджета внутри компании.
Само соглашение может быть простым — мы добавляем в стандарты кодирования клиентов две строки.
- Каждому элементу управления на экране, который потенциально может быть объектом операции или проверки, назначить осмысленное
Name(WinForms) /x:NameилиAutomationProperties.AutomationId(WPF) - Если на идентификатор уже ссылается тест, переименование выполнять одновременно с изменением на стороне теста (относиться к идентификатору как к публичному API)
5.2 Запрет кликов по координатам
Операции вида «кликнуть по экранным координатам (830, 412)» ломаются при изменении положения окна, разрешения, масштабирования DPI, темы или настроек шрифта. Особенно DPI массово порождает сбои вида «локально проходит, в CI падает» — из-за разницы окружения вроде 100% на машине разработчика и 150% на машине CI (о механизме DPI см. «Высокий DPI в WinForms»). Как мы видели в главе 2, операции через паттерны управления UIA не зависят от координат. В FlaUI есть API и для прямого управления мышью, но заранее решите, что их можно использовать только для операций, которые нельзя выразить через паттерн, — например, drag-and-drop или холст для рисования. И даже в этом случае вычисляйте относительное положение из BoundingRectangle элемента, а не из экранных координат.
5.3 Собираем структуру в одном месте с помощью паттерна Page Object
Если код поиска вроде FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox")) жёстко прописать прямо в теле теста, при изменении структуры экрана придётся править все тесты. Стандартное решение — паттерн Page Object: создать «один класс на экран (или диалог)» и заключить в нём поиск элементов и операции над ними.
public sealed class OrderDialogPage
{
private readonly Window _dialog;
public OrderDialogPage(Window dialog) => _dialog = dialog;
// Код поиска элементов пишется только внутри этого класса
private TextBox CustomerName =>
_dialog.FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox")).AsTextBox();
private Button Save =>
_dialog.FindFirstDescendant(cf => cf.ByAutomationId("SaveButton")).AsButton();
private Label Status =>
_dialog.FindFirstDescendant(cf => cf.ByAutomationId("StatusLabel")).AsLabel();
// Тесту видны только операции в терминах бизнес-логики
public void Register(string customerName)
{
CustomerName.Text = customerName;
Save.Invoke();
// Поиск элемента и проверку на null тоже выполняем внутри retry. На экранах,
// где метка статуса создаётся и перерисовывается после сохранения, то, что
// поиск на мгновение возвращает null или бросает исключение, — штатная ситуация
// (без ignoreException первое же исключение сразу же провалило бы вызов)
Retry.WhileFalse(() => Status?.Name.Contains("Сохранено") == true,
timeout: TimeSpan.FromSeconds(10), throwOnTimeout: true,
ignoreException: true);
}
}
Теперь тело теста можно писать в терминах бизнес-логики — new OrderDialogPage(dialog).Register("Тест Трейдинг ООО"), а влияние изменения экрана ограничивается одной правкой в Page Object. Если вы планируете продолжать эксплуатировать UI-тесты в приложении с более чем примерно 10 экранами, этот паттерн фактически обязателен. Единственное замечание: элементы, на которые ссылается условие ожидания, всегда ищите заново через свойство при каждом обращении, как Status выше. Если закэшировать результат поиска в поле, вы рискуете держать устаревший элемент, исчезнувший при перерисовке, и ждать вплоть до тайм-аута.
5.4 Независимость тестов — не передавать состояние между ними
Каждый UI-тест должен быть независимым. Если создать зависимость по порядку вроде «тест 3 предполагает данные, созданные тестом 2», сбой одного теста цепляет остальные, а заодно теряется возможность их переупорядочить. Принципы такие:
- Каждый тест (или тестовый класс) сам запускает приложение и гарантированно закрывает его по завершении (через
finallyили фикстуруIDisposable) - Исходные данные готовит сама тестовая сторона. Если дать приложению опцию запуска, позволяющую подменить конфигурационный файл или БД на тестовые, это резко упрощает работу
- Не забывать уборку при сбое. Незакрытый процесс или оставшийся модальный диалог от предыдущего сбоя становится причиной падения следующего теста (глава 7)
6. Что защищать UI-тестами — граница вокруг смоук-тестирования
Когда инструменты готовы, хочется протестировать все экраны, но именно здесь проходит водораздел. UI-тесты на порядки медленнее модульных (от секунд до десятков секунд каждый), более хрупкие (требуют доработки при каждом изменении UI) и дольше диагностируются при сбое (баг в приложении, баг в тесте или проблема окружения?). Именно эта структура затрат — причина, по которой верхушку пирамиды тестирования рисуют узкой: защищать UI-тестом то, что можно защитить нижним уровнем, — всегда потеря.
В виде таблицы решений это выглядит так.
| Что защищаем | Подходящий уровень | Причина |
|---|---|---|
| Вычисления, преобразования, бизнес-правила | Модульные тесты | Быстро и стабильно. Покрывать это через UI бессмысленно |
| Доступ к БД, файловый ввод-вывод, внешняя интеграция | Интеграционные тесты | Используем реальные компоненты, но UI не нужен (как проводить границу) |
| ViewModel, логика представления | Модульные тесты | При MVVM тестируется без UI |
| Запускается, основной сценарий проходит, можно сохранить | UI-смоук-тест | Здесь и разворачивается основная борьба за UI-тестирование |
| Критичные сценарии, ранее пропущенные при ручной проверке | UI-тест (регрессионный) | Добавлять точечно, только там, где был реальный ущерб |
| Разметка экрана, визуальные искажения | Визуальная проверка / сравнение скриншотов | Писать как assert — путь в ад сопровождения. Держать число небольшим |
| Крах, утечка хэндлов и прочие аномальные сценарии | Другая основа | Вне области UI-тестирования (Application Verifier) |
Мы рекомендуем начинать с «примерно 10 смоук-тестов»: «приложение запускается, появляется главный экран», «открываются ключевые справочники», «представительный документ можно зарегистрировать, найти и открыть предпросмотр печати», «при выходе не появляется ошибка». Автоматизируйте топ-10 сценариев, которые раньше обязательно проверяли вручную перед каждым релизом. При таком объёме написание займёт 1–2 недели, ежедневный ночной прогон уложится в 20–30 минут, а нагрузка на сопровождение останется реалистичной. Почувствовав эффект, добавляйте по одному регрессионные тесты для реально навредивших багов. И наоборот, план, нацеленный на «все экраны, все поля», почти наверняка развалится на полпути.
Ещё одно важное направление инвестиций — вытягивать логику из UI вместо увеличения числа UI-тестов. Экран, где бизнес-логика записана прямо в обработчиках событий, можно защитить только UI-тестом, но если перенести логику в ViewModel или сервисный класс, её можно защитить модульным тестом, а UI-тест будет лишь проверять «проводку». Если кажется, что нужных UI-тестов становится слишком много, по нашему опыту, дело чаще не в тестах, а в архитектуре. О том, как в целом разделять тесты по уровням, — в статье «Как провести границу между модульными и интеграционными тестами», а о практике тестов нижнего уровня — в статье «Минимальные требования к собственному логгеру и чек-лист интеграционного тестирования».
7. Ловушки CI и безлюдного запуска — без десктопа UI-тесты не работают
Пока написанные UI-тесты запускают вручную на своей машине, всё спокойно. Ловушки концентрируются на этапе «безлюдный ночной прогон в CI», и в отличие от Web UI-тестов (которые целиком выполняются в headless-браузере), корень всего в том, что тестирование десктопного приложения требует настоящей интерактивной десктопной сессии.
7.1 Интерактивная сессия обязательна — на агенте, запущенном как служба, ничего не работает
CI-агенты (агенты Azure Pipelines, самостоятельно размещённые runner-ы GitHub Actions и т. п.) обычно держат резидентно как службу Windows. Но у службы нет пользовательского рабочего стола, поэтому окнами приложения, запущенного из неё, управлять невозможно. Официальная документация Azure Pipelines прямо указывает, что агент, выполняющий UI-тесты десктопного приложения, должен быть настроен не как служба, а как интерактивный процесс с включённым автоматическим входом (autologon). 8 Также отмечается, что размещаемые Microsoft агенты (общие runner-ы, предоставляемые в облаке) вообще не поддерживают тесты с видимым UI — там работают только тесты в headless-браузере. 8 Иными словами, для UI-тестирования десктопного приложения фактически обязательна самостоятельно размещённая машина (физическая или виртуальная).
Официально отмечен и связанный с autologon риск безопасности: «любой, у кого есть физический доступ к этой машине, может воспользоваться автоматически вошедшей учётной записью». 8 Предпосылка — выделенная тестовая учётная запись на выделенной машине (VM), без production-учётных данных на ней.
7.2 Блокировка экрана, разрыв RDP, разрешение — типичные способы сломаться
Даже настроив интерактивную сессию, вы столкнётесь с ловушками. Сведём симптомы и меры в таблицу.
| Ловушка | Симптом | Мера |
|---|---|---|
| Агент запущен как служба | Элементы вообще не находятся, приложение не запускается | Перенастроить как интерактивный процесс + autologon 8 |
| Блокировка экрана / скринсейвер | Операции ввода не доходят до приложения и падают | Отключить скринсейвер в рамках настройки autologon. Запросить исключение из GPO, вызывающих блокировку 8 |
| Разрыв RDP по кнопке «×» | В момент разрыва сессия блокируется, все последующие тесты падают | Вернуть сессию на консоль командой tscon <ID сессии> /dest:console перед разрывом 8 |
| Разница окружения в разрешении/DPI | Тесты, проходящие локально, падают только в CI | Зафиксировать разрешение (в Azure Pipelines есть задача для этого). Унифицировать масштабирование на 100% 8 |
| Параллельный запуск тестов | Конкуренция за мышь, клавиатуру, фокус ломает тесты друг другом | UI-тесты запускать по одному, последовательно, на машину. Распараллеливать за счёт увеличения числа машин (VM) |
| Остатки предыдущего сбоя | Оставшийся процесс или модальный диалог мешает следующему запуску | Очищать целевые процессы перед началом теста. Обязательно выполнять завершающую обработку в finally |
Ловушка с RDP особенно легко подстерегает, поэтому дополним. Если зайти на тестовую машину по удалённому рабочему столу, что-то подправить, закрыть окно и выйти, эта сессия останется заблокированной, и все последующие UI-тесты будут продолжать падать. Официально рекомендуемый обходной путь — перед разрывом соединения выполнить в консоли администратора %windir%\System32\tscon.exe <ID> /dest:console, чтобы вернуть сессию на консоль. 8 Обязательно занесите это в регламент эксплуатации тестовой машины.
DPI и разрешение тоже требуют внимания. Нередко CI-VM остаётся на разрешении 1024×768 со стандартным масштабированием, из-за чего разметка меняется — возникает разница вроде «кнопка, видимая на машине разработчика, теперь требует прокрутки». Исключив клики по координатам, большую часть этого удаётся сгладить, но зафиксировать окружение всё равно лучше. Точки проверки, описанные в статье «Высокий DPI в WinForms», включая состояние поддержки DPI самим приложением, применимы здесь напрямую.
7.3 Сохраняем улики при сбое — скриншоты и логи
Когда безлюдный UI-тест падает, а в логе остаётся только «элемент не найден», причину понять невозможно. С самого начала закладывайте сохранение скриншота при сбое. В FlaUI есть функциональность захвата экрана или элемента — вызывайте её из хука сбоя тестового фреймворка и сохраняйте как артефакт CI.
// Вызывается, например, из хука сбоя. Выводит в каталог артефактов CI
FlaUI.Core.Capturing.Capture.Screen()
.ToFile(Path.Combine(artifactDir, $"{testName}_{DateTime.Now:HHmmss}.png"));
Помимо скриншотов, стоит сделать так, чтобы собственный лог приложения (докуда дошла обработка) и лог теста (какие операции прошли успешно) можно было сопоставить по временным меткам — это резко ускоряет разделение «баг приложения, баг теста или окружение». Об особенностях запуска тестов по ночам через Планировщик задач (типы сессий, завершение с кодом 0x1 и т. п.) — в статье «Задачи Планировщика заданий не выполняются или завершаются с 0x1».
8. Итог
Автоматическое UI-тестирование — это не то, что «просто заработает после установки инструмента»: оно продолжает работать только тогда, когда сходятся понимание механизма, содействие со стороны приложения, чёткая граница области покрытия и продуманная среда выполнения. Сожмём ключевые моменты.
- В основе — UI Automation. Ищем в дереве по AutomationId, управляем через паттерны управления. Сначала — инвентаризация видимости целевого приложения через inspect / Accessibility Insights
- Инструменты — FlaUI + xUnit / NUnit. У WinAppDriver с 2020 года нет стабильных релизов — избегайте его для новых проектов
- Устойчивость строится через соглашения. AutomationId проставляет разработка, клики по координатам под запретом, Sleep запрещён в пользу условного ожидания через Retry, структура собрана в одном месте через Page Object
- Область покрытия — начиная с примерно 10 смоук-тестов. Логику выносим в модульные и интеграционные тесты, а UI-тесты сосредотачиваем исключительно на проверке «основной сценарий работает»
- Базовая форма CI — самостоятельно размещённая машина + интерактивная сессия + autologon. Ловушки блокировки экрана, разрыва RDP и разницы в разрешении закрываются регламентом эксплуатации, а скриншот при сбое сохраняется обязательно
«Два дня ручной проверки перед каждым релизом» реально заменить с разумными затратами правильно ограниченными автоматическими UI-тестами. И наоборот: если в вашем текущем приложении AutomationId вообще нигде не проставлен или логика записана прямо в события экрана, в итоге быстрее начать с небольших доработок приложения, прежде чем писать тесты. Мы можем помочь начиная с обследования текущего состояния приложения — включая то, откуда начинать, и разворачивание набора смоук-тестов.
Похожие статьи
- Как провести границу между модульными и интеграционными тестами
- Строим основу для тестирования аномальных сценариев Windows с Application Verifier
- Минимальные требования к собственному логгеру и чек-лист интеграционного тестирования
- Поддержка тестов PowerShell через Pester — практический подход к тому, чтобы эксплуатационные скрипты было сложнее сломать
- Высокий DPI в WinForms — почему всё размывается и ломается на 4K-мониторах и как это реально исправить
Смежные области консультаций
Komura Software Co., Ltd. занимается внедрением автоматического UI-тестирования в приложениях на WinForms / WPF (обследование текущего состояния, настройка AutomationId, построение полного набора смоук-тестов, проектирование CI-окружения), миграцией существующих тестовых активов на FlaUI, а также ревью общей стратегии тестирования.
- Техническая консультация / ревью архитектуры
- Разработка Windows-приложений
- Использование и миграция существующих активов
- Контакты
Источники
-
Microsoft Learn, UI Automation Overview. О дереве автоматизации, корнем которого выступает рабочий стол, о raw-, control- и content-представлениях, о свойствах элементов и паттернах управления, а также о том, что вспомогательные технологии и автоматизация тестирования используют одну и ту же платформу. ↩ ↩2 ↩3
-
Microsoft Learn, UI Automation Control Patterns Overview. О проектировании паттернов управления (аналогия с интерфейсами COM), о паттернах вроде Invoke / Value / SelectionItem и о том, что один элемент управления может реализовывать несколько паттернов. ↩ ↩2
-
Microsoft Learn, Accessibility tools - Inspect. О том, что inspect.exe поставляется вместе с Windows SDK и позволяет посмотреть UIA-свойства и паттерны, и о том, что он позиционируется как устаревший инструмент, а рекомендуется Accessibility Insights. ↩ ↩2 ↩3
-
GitHub, FlaUI/FlaUI. О поддержке Win32 / WinForms / WPF / приложений из Store в качестве обёртки над UIA, о структуре пакетов FlaUI.Core / UIA2 / UIA3, о рекомендациях по выбору между UIA2 и UIA3 (FAQ), о лицензии MIT, о релизе v5.0.0 (февраль 2025) и продолжающейся разработке, об утилите Retry и об отказе от неявных повторов в методах Find начиная с версии 2.0. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
GitHub, microsoft/WinAppDriver. О том, что последний стабильный релиз v1.2.1 вышел в ноябре 2020 года, что v1.3 так и остаётся неопубликованным Release Candidate от июля 2020 года, что нерешённых issue более 1100, и о том, что в репозитории в основном документация и примеры, а исходный код самого сервера не опубликован. ↩ ↩2 ↩3
-
Microsoft Learn, Use the AutomationID Property. О том, что AutomationId — независимый от локали идентификатор, должен быть уникален среди соседних элементов, о его использовании как ключа поиска в тестовых скриптах и о том, что в WPF элементы без ID (x:Name) или x:Uid не поддерживают AutomationId. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, AutomationProperties.AutomationId Attached Property. Об определении присоединённого свойства (в пространстве имён System.Windows.Automation) для задания строки, однозначно идентифицирующей элемент в WPF. ↩ ↩2
-
Microsoft Learn, UI testing considerations (Azure Pipelines). О том, что для UI-тестирования десктопных приложений агент должен быть настроен как интерактивный процесс с включённым autologon, что размещаемые Microsoft агенты не поддерживают тесты с видимым UI, о блокировке при разрыве RDP и обходе через tscon, о задаче настройки разрешения экрана и о сборе скриншотов/видео при сбое. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
GitHub, appium/appium-windows-driver. О том, что Appium Windows Driver — интерфейс к WinAppDriver от Microsoft, и о предупреждении в README о том, что сервер WinAppDriver долгое время не сопровождается. ↩ ↩2
-
Microsoft Learn, Use Coded UI tests to test your code. О том, что Coded UI Tests признаны устаревшими, Visual Studio 2019 — последняя версия с полной поддержкой, и о том, что в качестве пути миграции для десктопных / UWP-приложений рекомендовались Appium + WinAppDriver (удаление в VS 2026 описано в том же руководстве по миграции на Learn). ↩ ↩2
-
Microsoft Learn, WinForms: Setting the accessible name on a control. О том, что свойство UIA Name у многих типов элементов управления WinForms берётся из свойства Text, и о том, что для типов вроде TextBox / ListBox нужно явно задавать AccessibleName. ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
CI/CD для приложений WinForms / WPF на практике — автоматизация от сборки до подписи и распространения через GitHub Actions
Практическое руководство по настройке CI/CD для приложений WinForms / WPF через GitHub Actions. Минимальный YAML для сборки и тестов на w...
Встраиваем аутентификацию Entra ID в приложения WinForms/WPF — практическая архитектура на MSAL.NET и брокере WAM
Разбираем порядок встраивания аутентификации Entra ID в десктопные приложения WinForms/WPF: концепцию публичного клиента, регистрацию при...
Значки в области уведомлений и всплывающие (toast) уведомления в Windows-приложениях — подводные камни NotifyIcon и выбор правильного AppNotification
Практическое руководство о том, как удерживать бизнес-приложение Windows в области уведомлений (system tray) и оповещать пользователя с п...
Дата, время и часовые пояса в бизнес-приложениях — от ловушек DateTime до принципа хранения в UTC и проектирования тестов
Перенос сервера сдвигает время на 9 часов — разбираем причины подобных сбоев начиная со свойства Kind у DateTime и неявных преобразований...
Высокий DPI в WPF — почему всё «должно быть само в порядке», а на деле размывается и плывёт, и что с этим делать
WPF по умолчанию System DPI Aware, но при переносе окна на монитор с другим DPI всё изображение размывается, а растровые картинки становя...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Поток UI и таймеры
Поток UI WPF / WinForms, асинхронные операции, Dispatcher и проектирование таймеров.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Что использовать для автоматического UI-тестирования приложений на WinForms/WPF?
- Наша рекомендация - FlaUI в связке с xUnit/NUnit. FlaUI - это OSS-библиотека под лицензией MIT, тонко оборачивающая Windows UI Automation (UIA), поддерживает и UIA2, и UIA3, и активно поддерживается: в феврале 2025 вышла версия v5.0.0. Практический ориентир - пробовать UIA3 для WPF и UIA2 для WinForms. Собственного тестового фреймворка у неё нет, поэтому тесты запускаются тем же test runner-ом и в том же CI-конвейере, что и модульные тесты. Первым делом, ещё до написания кода, стоит посмотреть, как элементы целевого приложения видны через inspect.exe или Accessibility Insights for Windows.
- Стоит ли сейчас брать WinAppDriver для новых проектов?
- Мы не рекомендуем. Последняя стабильная версия WinAppDriver от Microsoft, v1.2.1, вышла в ноябре 2020 года, версия v1.3 так и осталась Release Candidate от июля 2020, а число нерешённых issue превышает 1100. Более того, в GitHub-репозитории есть только документация и примеры - исходный код самого сервера закрыт, поэтому даже сообщество не может исправлять баги. Windows Driver в составе Appium тоже внутри использует WinAppDriver и наследует те же ограничения. Если у вас уже есть тестовые активы на WinAppDriver, немедленно выбрасывать их не нужно, но новые сценарии тестирования стоит переводить на FlaUI.
- Как предотвратить хрупкость автоматических UI-тестов?
- На 80% устойчивость определяется соглашением на стороне разработки о простановке AutomationId. В WPF это x:Name или AutomationProperties.AutomationId, в WinForms - Control.Name. Помимо этого нужно запретить поиск по отображаемой строке (Name) и клики по координатам, заменить Thread.Sleep условным ожиданием через класс Retry из FlaUI, а поиск и операции с элементами каждого экрана собрать в одном месте с помощью паттерна Page Object. Начиная с версии 2.0 FlaUI отказалась от неявных повторов в методах Find, поэтому поиск асинхронно появляющихся элементов нужно всегда оборачивать в Retry - закрепите это как правило.
- До какого предела стоит писать автоматические UI-тесты?
- Рекомендуем начинать примерно с 10 смоук-тестов. UI-тесты на порядки медленнее и хрупче модульных, поэтому вычисления и бизнес-правила защищают модульными тестами, доступ к БД - интеграционными, а UI-тесты ограничивают проверкой того, что «приложение запускается и основные сценарии проходят». Если автоматизировать топ-10 сценариев, которые раньше проверяли вручную перед релизом, написание займёт 1-2 недели, а ежедневный ночной прогон уложится в 20-30 минут. План, нацеленный на «все экраны, все поля», почти наверняка развалится. Если кажется, что нужных UI-тестов становится слишком много, это сигнал вынести логику в ViewModel и защитить её модульными тестами.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки