UI-автотесты десктопных приложений Windows — UI Automation и устойчивые тесты на FlaUI

· Обновлено: · · Тестирование, UI Automation, FlaUI, WinForms, WPF, C#, .NET, Windows, CI/CD

История изменений (2 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Переведено имя метода в примере UI-автотеста. Утверждения статьи не менялись.
Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619947)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). UI-автотесты десктопных приложений Windows — UI Automation и устойчивые тесты на FlaUI. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-desktop-ui-automation-testing/

DOI (зарегистрированный архив)
10.5281/zenodo.21619947
DOI (последняя зарегистрированная версия)
10.5281/zenodo.21619948

«Перед каждым релизом уходит целых два дня: все экраны прокликиваем вручную». «В прошлом месяце починили одно место — рядом сломался другой экран, и нашли это уже у заказчика». «Веб-команда гоняет регрессию через Selenium или Playwright, а десктоп так и остаётся на ручных проверках». Такие разговоры мы часто слышим у команд, которые годами сопровождают бизнес-приложения на WinForms / WPF.

Столь же часто слышим и обратное: взялись покрыть все экраны, сопровождение не выдержали и через год бросили. Если ошибиться с инструментом и с границей «докуда автоматизировать», UI-автотесты обходятся дороже ручных проверок. Если разобраться в механизме, спроектировать тесты так, чтобы они не рассыпались при правках UI, и ограничить покрытие дымовыми сценариями, «два дня перед релизом» превращаются в «двадцать минут ночью без человека». Ниже — тот каркас, которым мы пользуемся на реальных проектах: устройство UI Automation (UIA), состояние инструментов (FlaUI / WinAppDriver / Appium), реализация на FlaUI, приёмы устойчивости и запуск без оператора в CI.

1. Главное

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

  • 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 — открытая 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 тоже валят прогон. Базовая схема — self-hosted runner с автоматическим входом (autologon) (глава 7). 8

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 24, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Как это устроено: дерево, свойства и шаблоны UI Automation

2.1 Дерево автоматизации

Инструменты UI-автотестов не «видят» экран как картинку. В Windows есть официальная платформа UI Automation (UIA): программы чтения с экрана и прочие вспомогательные технологии читают и нажимают UI приложения программно. Автотесты едут по тем же рельсам. 1

В UIA корень — рабочий стол, а все открытые окна и элементы управления внутри них опубликованы деревом. Кнопка, текстовое поле, строка грида — всё это элементы автоматизации на этом дереве. Кроме raw view, где видны все узлы, есть отфильтрованные представления: control view — только то, чем можно управлять, и content view — только то, что несёт информацию пользователю. 1 Тестовый код в основном ходит по control view.

Три представления — не три независимых дерева, а разная сила фильтра на одном и том же дереве. Включение вложенное.

raw view — все элементы дереваcontrol view — элементы, которыми можно управлятьcontent view — элементы, которые передают информацию пользователюкнопка SaveButtonтекстовое поле CustomerNameBoxэлемент списка ООО Тест Трейдингзаголовок окнаполоса прокруткирамка группыдекоративный разделительпромежуточный элемент только для вёрстки

Читается так: «всё, что есть в content view, есть и в control view; всё, что есть в control view, есть и в raw view». Элементы, которые ищет тест, почти всегда лежат во внутренних двух. Если зацепить поиск за узел, который виден только в raw view (контейнер «для вёрстки»), достаточно слегка поменять разметку — и тест падает. В inspect.exe эти представления переключаются в меню Options пунктами «Raw View», «Control View», «Content View». Когда будете снимать картину в разделе 2.3, сразу смотрите, до какого представления доживает нужный элемент — так проще проектировать условие поиска. 3

Главное здесь: управлять можно не тем, что видно глазом, а тем, что опубликовано в дереве. Экраны из стандартных контролов попадают в дерево аккуратно. Owner-drawn список, область чертежа, которую рисует графическая библиотека, кусок стороннего грида — в дереве это нередко «один узел». Эта видимость и есть потолок UI-автотестов. Поэтому первый шаг — не код, а проверка дерева (раздел 2.3).

2.2 Свойства и шаблоны элементов управления

Чтобы найти нужный узел в дереве, нужны свойства. На практике хватает четырёх.

Свойство Что внутри Роль в тесте
AutomationId Идентификатор, который задаёт разработчик. Не зависит от языка (локали) Основной ключ поиска. Должен быть уникален среди соседних элементов 6
Name Имя из отображаемого текста (подпись кнопки и т. п.) Человеку понятно, но ломается при смене формулировки и при локализации
ControlType Тип: Button / Edit / ComboBox и другие Вспомогательный фильтр
ClassName Имя класса реализации (класс WinForms и т. п.) Последнее средство. Хрупко при смене реализации

По спецификации AutomationId «должен оставаться тем же при смене локали» и «должен быть уникален среди соседних элементов»: это свойство как раз для того, чтобы UI-автотест стабильно находил элемент и при другом языке, и в другой версии. 6 Если AutomationId не задан, тест вынужден цепляться за отображаемый текст или за положение в дереве — оба признака хрупкие. Этому посвящена глава 5.

Действия над найденным элементом идут через шаблоны элементов управления (control patterns). UIA публикует функциональность контрола — «можно нажать», «есть значение», «можно выбрать» — набором шаблонов, отдельным от типа контрола. Документация прямо сравнивает связь шаблона и UI со связью COM-объекта и его интерфейсов: у элемента спрашивают, какие шаблоны он реализует, и вызывают действие через шаблон. 2 Кто знаком с COM, может думать об этом как о QueryInterface для UI. Основные шаблоны такие.

Шаблон Что умеет Типичные контролы
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-свойств и шаблонов, по дереву тоже можно пройтись. Официально это legacy-инструмент, рекомендуют переходить на Accessibility Insights. 3
  • Accessibility Insights for Windows: текущая рекомендация Microsoft. Live Inspect удобен: навели мышь или сдвинули фокус — видны UIA-свойства. Есть и автоматическая проверка специальных возможностей (FastPass). 3
  • FlaUInspect: инспектор из проекта FlaUI. Дерево можно смотреть глазами UIA2 и UIA3 — теми реализациями, которыми реально ходит FlaUI. Если тесты пишете на FlaUI, его тоже стоит поставить. 4

Куда смотреть в inspect.exe

Вместо скриншота опишем, что в каком месте окна. inspect.exe лежит в bin\<версия>\<платформа> установки Windows SDK; запускать от администратора обычно не нужно. После старта он по умолчанию следит за фокусом клавиатуры или мыши, сведения о выбранном элементе — справа. 3

Часть окна Что показывает Что смотреть для теста
Дерево слева Иерархия элементов с корнем в рабочем столе. Здесь проверяют родителя и потомков Есть ли нужный элемент в дереве и под каким родителем
Список данных справа UIA-свойства выбранного элемента: имя и значение AutomationId, Name, ControlType, ClassName, IsEnabled, BoundingRectangle
Меню Options Режим отображения и способ слежения «UI Automation Mode» (не MSAA Mode), «Raw View» / «Control View» / «Content View», «Watch Focus» / «Watch Cursor», «Show Highlight Rectangle»
Меню Action Можно вызвать UIA-методы выбранного элемента В режиме UI Automation здесь же стоят шаблоны элементов управления (раздел 2.2), которые элемент реализует
Options > Settings Какие свойства показывать в списке данных Если строк слишком много — сузить список. «Display unsupported properties» показывает и неподдерживаемые свойства

Практика такая. Сначала убедитесь, что включён «UI Automation Mode», включите «Watch Focus» и походите по целевому приложению с клавиатуры: при каждом сдвиге фокуса список справа обновляется. Смотрите, не пуст ли AutomationId. Пуст — сначала доработка приложения из главы 5. Затем откройте Action и проверьте: у кнопки, которую хотите нажать, есть Invoke, у поля, в которое хотите ввести текст, есть Value. То, что здесь перечислено, и есть операции, которые вызовете из теста. Меню и подсказки, на которые фокус сам не встаёт, ловят переключением на «Watch Cursor». 3

По опыту, при внедрении UI-автотестов первым делом делают не «написать тест», а открыть основные экраны в inspect-подобном инструменте и посмотреть, насколько густо расставлен AutomationId. Если здесь пусто, быстрее начать с доработки приложения (глава 5).

3. Выбор инструмента: почему FlaUI и что с WinAppDriver

В UIA можно ходить напрямую через COM, но на практике берут библиотеку-обёртку. Ниже — варианты на 2026 год по проверенным фактам.

Инструмент Вид Состояние (на 2026 год) Ориентир для нового проекта
FlaUI .NET-библиотека (MIT) Живой open source. v5.0.0 вышла в феврале 2025 4 ◎ Первый выбор
WinAppDriver Сервер протокола WebDriver (Microsoft) Последняя стабильная v1.2.1 — ноябрь 2020. v1.3 так и осталась RC от июля 2020. Нерешённых issue больше 1100 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 — только managed-реализация, новые возможности вроде touch не поддерживает и с WPF и Store-приложениями дружит плохо. UIA3 — более новая реализация, для WPF / Store она оптимальна, но на WinForms иногда всплывают баги, которых нет в UIA2. 4 Практичный ориентир: для WPF начинать с UIA3, для WinForms — с UIA2, и оставить ту, которая стабильнее на вашем приложении. Оба пакета ставятся из NuGet и переключаются — в этом как раз плюс тонкой обёртки.

Своего тестового фреймворка нет, поэтому FlaUI сочетают с xUnit / NUnit / MSTest и пишут обычный тестовый проект. Тот же test runner и тот же CI-конвейер, что у модульных тестов, заметно упрощает эксплуатацию.

3.2 WinAppDriver — если честно, он фактически стоит

WinAppDriver — UI-тестовый сервер 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 Хуже другое: в репозитории — документация, примеры и трекер 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: запуск, поиск, действие, проверка

Теории достаточно — смотрим рабочий код. Сначала соберём в одном месте предпосылки, которые дальше по тексту разбросаны.

Что подготовить

Вид Что нужно Замечание
Целевое приложение Собранный exe, полный путь Если есть ключ запуска, которым подменяют конфиг и БД на тестовые, жить сразу проще (раздел 5.4)
SDK .NET SDK (рекомендуем 8 и новее) UIA только под Windows, поэтому target тестового проекта — net8.0-windows
NuGet FlaUI.UIA3 (для WPF) или FlaUI.UIA2 (для WinForms) Общая часть FlaUI.Core подтянется зависимостью. Можно поставить оба и переключать (раздел 3.1) 4
NuGet xunit + xunit.runner.visualstudio Ставится через dotnet new xunit. NUnit / MSTest тоже подходят
Инспектор inspect.exe или Accessibility Insights for Windows До кода снимите, как приложение видно в дереве (раздел 2.3) 3
Среда запуска Windows с рабочим столом. Пока тесты идут, машину руками не трогать Условия для запуска без оператора — глава 7

Проект создаётся тремя командами.

dotnet new xunit -o OrderManager.UiTests
cd OrderManager.UiTests
dotnet add package FlaUI.UIA3

В получившемся .csproj поменяйте TargetFramework на net8.0-windows (UIA только под Windows). UI-тесты на одной машине нужно гонять по одному, последовательно (раздел 7.2), поэтому параллельный запуск xUnit сразу выключите.

// Один раз в тестовом проекте (например, AssemblyInfo.cs)
using Xunit;

[assembly: CollectionBehavior(DisableTestParallelization = true)]

Дальше это обычный тестовый проект. Ни отдельный runner, ни отдельный тип проекта не нужны.

4.1 Базовая форма дымового теста

Сценарий «запустить приложение, открыть диалог регистрации заказа, сохранить одну запись, увидеть результат в статусе» пишется так.

using FlaUI.Core;
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Tools;
using FlaUI.UIA3;
using Xunit;

public class OrderSmokeTest
{
    [Fact]
    public void РегистрацияЗаказа_ОсновнойСценарийПроходит()
    {
        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 — уже проиграли

Главный источник нестабильности UI-тестов (flaky test) — тайминг. Пока откроется диалог, пока доедут данные, пока кнопка станет enabled — 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 (кнопка станет enabled)
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%, на CI 150% (механизм DPI — в «Поддержка высокого DPI в WinForms — почему на 4K размывается или ломается макет»). Как видно из главы 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-тесты собираетесь вести дальше, этот паттерн по сути обязателен. Одно замечание: элемент, на который ссылается условие ожидания, каждый раз ищите заново через свойство, как 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 сценариев, которые раньше обязательно прокликивали руками перед релизом. При таком объёме написание занимает 1–2 недели, ночной прогон укладывается в 20–30 минут, нагрузка на сопровождение остаётся реальной. Почувствовав эффект, добавляйте по одному регрессионные тесты на баги, которые уже навредили. План «все экраны, все поля» почти наверняка развалится на полпути.

Второй тип инвестиций: вместо роста числа UI-тестов выносить логику из UI. Экран, где бизнес-логика сидит в обработчиках событий, закрывается только UI-тестом. Если логику перенести в ViewModel или сервисный класс, её закрывают модульным тестом, а UI-тест проверяет лишь «проводку». Если кажется, что UI-тестов нужно слишком много, по опыту это чаще вопрос устройства кода, а не тестов. Как вообще раскладывать тесты по слоям — в «Где провести границу между юнит-тестами и интеграционными тестами», практика нижнего слоя — в «Минимальные требования к самописному логгеру и чек-лист интеграционных тестов».

7. Ловушки CI и запуска без оператора: без рабочего стола UI-тесты не едут

Пока UI-тесты гоняют с машины разработчика, всё спокойно. Ловушки собираются на шаге «каждую ночь без человека в CI». В отличие от Web UI-тестов (их хватает headless-браузера), тест десктопного приложения требует настоящей интерактивной сессии рабочего стола — из этого растут все остальные проблемы.

7.1 Интерактивная сессия обязательна: агент-служба не подойдёт

CI-агенты (агент Azure Pipelines, self-hosted runner GitHub Actions и т. п.) обычно крутят как службу Windows. У службы нет пользовательского рабочего стола, поэтому окнами приложения, которое она запустила, управлять нельзя. Официальная документация Azure Pipelines прямо говорит: агент для UI-тестов десктопного приложения нужно поднимать не как службу, а как интерактивный процесс с включённым автоматическим входом (autologon). 8 Microsoft-hosted агенты (общие облачные runner’ы) тесты с видимым UI не поддерживают вообще — там живут только тесты в headless-браузере. 8 Итого: для UI-тестов десктопного приложения фактически нужна своя машина (физическая или VM).

У autologon официально отмечен и риск: «у кого есть физический доступ к машине, тот может сесть под автоматически вошедшую учётную запись». 8 Исходим из отдельной тестовой учётки на отдельной машине (VM), без боевых учётных данных.

7.2 Блокировка экрана, разрыв RDP, разрешение — типичные падения

Интерактивную сессию настроили — ловушки остаются. Симптомы и меры — в таблице.

Ловушка Симптом Что делать
Агент запущен как служба Элементы не находятся вовсе, приложение не стартует Поднять как интерактивный процесс + autologon 8
Блокировка экрана / заставка Операции ввода не доходят, тест падает В схеме autologon отключить заставку. Запросить исключение из GPO, которые включают блокировку 8
RDP закрыли кнопкой «×» В момент разрыва сессия блокируется, все следующие тесты красные Перед разрывом вернуть сессию на консоль: tscon <ID сессии> /dest:console 8
Разница разрешения / DPI Локально зелёное, в CI красное Зафиксировать разрешение (в Azure Pipelines есть задача). Масштаб унифицировать на 100% 8
Параллельный запуск тестов Борьба за мышь, клавиатуру и фокус ломает тесты друг о друга UI-тесты — по одному на машину, последовательно. Параллелить числом машин (VM)
Хвосты прошлого падения Оставшийся процесс или модальный диалог мешает следующему запуску Перед тестом чистить целевые процессы. Завершение всегда в finally

Ловушка с RDP особенно частая, поэтому отдельно. Зашли на тестовую машину по Remote Desktop, что-то поправили, закрыли окно и вышли — сессия остаётся заблокированной, и UI-тесты дальше падают. Официальный обход: до разрыва в командной строке администратора выполнить %windir%\System32\tscon.exe <ID> /dest:console и вернуть сессию на консоль. 8 Занесите это в регламент тестовой машины.

DPI и разрешение тоже стоит зафиксировать. Нередко CI-VM остаётся на 1024×768 со стандартным масштабом: вёрстка плывёт, «кнопка, которую на машине разработчика было видно, теперь за прокруткой». Без кликов по координатам большая часть этого сходит на нет, но окружение всё равно лучше закрепить. Точки проверки из «Поддержка высокого DPI в WinForms — почему на 4K размывается или ломается макет», включая состояние DPI у самого приложения, здесь те же.

7.3 Оставляем следы падения: скриншот и логи

Когда прогон без оператора падает, а в логе только «элемент не найден», причины не видно. С самого начала закладывайте сохранение скриншота при падении. В FlaUI есть захват экрана или элемента — вызывайте его из хука падения тестового фреймворка и кладите в артефакты CI.

// Вызывать, например, из хука падения. Пишем в каталог артефактов CI
FlaUI.Core.Capturing.Capture.Screen()
    .ToFile(Path.Combine(artifactDir, $"{testName}_{DateTime.Now:HHmmss}.png"));

Помимо скриншота стоит уметь стыковать по времени собственный лог приложения (до какого шага дошла обработка) и лог теста (какие операции уже прошли) — так быстрее понять, баг приложения, баг теста или окружение. Если прогон ночью крутит Планировщик заданий (тип сессии, завершение с 0x1 и т. п.) — это разобрано в «Планировщик заданий не запускается или завершается с 0x1 — диагностика и надёжная эксплуатация».

7.4 Минимальный пример CI: self-hosted runner GitHub Actions

Условия из 7.1–7.3 в workflow выглядят так. Главное: runner поднимают не как службу Windows, а как интерактивный процесс, на тестовой машине с включённым автоматическим входом (раздел 7.1). Если это не так, YAML может быть идеальным — тест всё равно не найдёт ни одного элемента.

name: nightly-ui-smoke

on:
  schedule:
    # cron задаётся в UTC. Чтобы гонять в 0:00 JST по будням, в UTC это вс–чт 15:00
    - cron: "0 15 * * 0-4"
  workflow_dispatch:   # оставляем и ручной запуск для разбора

jobs:
  ui-smoke:
    # Метка self-hosted runner, который крутится в интерактивной сессии.
    # На GitHub-hosted runner тесты с видимым UI не работают (раздел 7.1)
    runs-on: [self-hosted, windows, ui-test]
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4

      - name: Убрать хвосты прошлого прогона
        shell: powershell
        continue-on-error: true
        run: Get-Process OrderManager -ErrorAction SilentlyContinue | Stop-Process -Force

      - name: Собрать приложение
        run: dotnet publish src/OrderManager -c Release -o artifacts/app

      - name: Запустить дымовые UI-тесты
        run: dotnet test tests/OrderManager.UiTests -c Release --logger "trx;LogFileName=ui.trx"

      - name: Собрать артефакты
        if: always()   # нужны как раз когда упало, поэтому always
        uses: actions/upload-artifact@v4
        with:
          name: ui-test-evidence
          path: |
            **/TestResults/**
            artifacts/screenshots/**

Четыре якоря.

  • runs-on — метка self-hosted. Как в разделе 7.1, общие hosted runner тесты с видимым UI не поддерживают. 8
  • timeout-minutes обязателен. UI-тест, который застрял на модальном диалоге, сам не вернётся — его режет job.
  • Уборку пишите отдельным шагом. Хвост прошлого падения (незакрытый процесс) мешает следующему запуску (разделы 5.4 и 7.2).
  • Артефакты снимайте при if: always(). Без скриншотов из раздела 7.3 (в примере — artifacts/screenshots) и trx с результатами падение ночного прогона не разобрать.

Время в schedule нужно писать в UTC — это не только GitHub Actions. Классическая ошибка: написать «как в Японии» и получить сдвиг на сутки (дата, время и пояса — в «Дата, время и часовые пояса в бизнес-приложениях — от ловушек DateTime до принципа хранения в UTC и проектирования тестов»).

8. Итог

UI-автотест — не «поставили инструмент и поехало». Он продолжает жить, только когда сходятся понимание механизма, готовность приложения публиковать идентификаторы, узкая граница покрытия и продуманная среда запуска. Коротко:

  • Основа — UI Automation. В дереве ищем по AutomationId, действуем через шаблоны элементов управления. Сначала снимаем, как целевое приложение видно в inspect / Accessibility Insights
  • Инструмент — FlaUI + xUnit / NUnit. У WinAppDriver с 2020 года нет стабильных релизов, в новый проект его не берём
  • Устойчивость делают соглашения. AutomationId задаёт разработка, клики по координатам запрещены, Sleep запрещён в пользу Retry, структура собрана в Page Object
  • Покрытие — примерно десять дымовых тестов. Логику уводим в модульные и интеграционные тесты, UI проверяет только «основной сценарий проходит»
  • CI — своя машина + интерактивная сессия + autologon. Блокировку экрана, разрыв RDP и разницу разрешения закрывают регламентом, скриншот при падении сохраняют всегда

«Два дня ручных проверок перед релизом» реально заменить, если UI-автотесты правильно сузить. Если в текущем приложении AutomationId нигде нет или логика сидит прямо в событиях экрана, быстрее начать с небольшой доработки приложения, а не с тестов. Можем помочь с оценкой, с чего начинать, и с подъёмом набора дымовых тестов — начиная с обследования текущего приложения.

Похожие статьи

Смежные области консультаций

Komura Soft внедряет UI-автотесты в приложения на WinForms / WPF (обследование текущего состояния, настройка AutomationId, набор дымовых тестов, проектирование CI), переводит существующие тесты на FlaUI и делает ревью стратегии тестирования в целом.

Источники

  1. Microsoft Learn, UI Automation Overview. О дереве автоматизации с корнем в рабочем столе, о raw / control / content view, о свойствах элементов и шаблонах элементов управления, о том, что вспомогательные технологии и автотесты используют одну платформу.  2 3

  2. Microsoft Learn, UI Automation Control Patterns Overview. О устройстве шаблонов элементов управления (аналогия с интерфейсами COM), о шаблонах Invoke / Value / SelectionItem и о том, что один контрол может реализовывать несколько шаблонов.  2

  3. Microsoft Learn, Accessibility tools - Inspect. О том, что inspect.exe идёт в Windows SDK (bin\<version>\<platform>) и показывает UIA-свойства и шаблоны; что это legacy-инструмент и рекомендуется Accessibility Insights; что окно состоит из дерева и списка данных и по умолчанию следит за фокусом; о пунктах Options (UI Automation Mode / Raw View / Control View / Content View / Watch Focus / Watch Cursor / Show Highlight Rectangle / Settings) и о том, что из меню Action можно вызвать шаблоны элементов управления выбранного узла.  2 3 4 5 6 7

  4. GitHub, FlaUI/FlaUI. Обёртка над UIA для Win32 / WinForms / WPF / приложений Store, пакеты FlaUI.Core / UIA2 / UIA3, выбор UIA2 и UIA3 (FAQ), лицензия MIT, релиз v5.0.0 (февраль 2025) и продолжающаяся разработка, утилита Retry и отказ от неявных повторов Find начиная с версии 2.0.  2 3 4 5 6 7 8 9

  5. GitHub, microsoft/WinAppDriver. Последний стабильный релиз v1.2.1 — ноябрь 2020, v1.3 так и остаётся Release Candidate от июля 2020, нерешённых issue больше 1100, в репозитории в основном документация и примеры, исходный код сервера не опубликован.  2 3

  6. Microsoft Learn, Use the AutomationID Property. О том, что AutomationId не зависит от локали, должен быть уникален среди соседних элементов, используется как ключ поиска в тестовых скриптах, и о том, что в WPF контролы без ID (x:Name) или x:Uid AutomationId не поддерживают.  2 3 4 5

  7. Microsoft Learn, AutomationProperties.AutomationId Attached Property. Об определении присоединённого свойства в пространстве имён System.Windows.Automation: строка, однозначно идентифицирующая элемент в WPF.  2

  8. Microsoft Learn, UI testing considerations (Azure Pipelines). Для UI-тестов десктопных приложений агент нужно поднимать как интерактивный процесс с autologon; Microsoft-hosted агенты тесты с видимым UI не поддерживают; блокировка при разрыве RDP и обход через tscon; задача настройки разрешения экрана; сбор скриншотов и видео при падении.  2 3 4 5 6 7 8 9 10

  9. GitHub, appium/appium-windows-driver. Appium Windows Driver — интерфейс к WinAppDriver от Microsoft; в README предупреждают, что сервер WinAppDriver долго не сопровождают.  2

  10. Microsoft Learn, Use Coded UI tests to test your code. Coded UI Tests устарели, Visual Studio 2019 — последняя версия с полной поддержкой; для десктопа / UWP указывали путь Appium + WinAppDriver (удаление в VS 2026 описано в том же руководстве по миграции на Learn).  2

  11. Microsoft Learn, WinForms: Setting the accessible name on a control. О том, что свойство UIA Name у многих типов контролов WinForms берётся из Text, и о том, что для TextBox / ListBox нужно явно задавать AccessibleName. 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Чем автоматизировать UI-тесты WinForms/WPF-приложения?
Мы рекомендуем FlaUI вместе с xUnit или NUnit. FlaUI — открытая MIT-библиотека, тонкая обёртка над Windows UI Automation (UIA): есть и UIA2, и UIA3, проект живой (v5.0.0 вышла в феврале 2025). Практичный старт: для WPF пробовать UIA3, для WinForms — UIA2. Своего тестового фреймворка у FlaUI нет, поэтому тесты идут тем же test runner и тем же CI-конвейером, что и модульные. Прежде чем писать код, посмотрите, как элементы целевого приложения видны в inspect.exe или Accessibility Insights for Windows.
Стоит ли сейчас брать WinAppDriver в новый проект?
Для нового проекта — нет. Последняя стабильная версия Microsoft WinAppDriver, v1.2.1, вышла в ноябре 2020 года; v1.3 так и осталась Release Candidate от июля 2020, нерешённых issue больше 1100. В GitHub-репозитории — документация и примеры, исходный код самого сервера закрыт, сообщество баги чинить не может. Appium Windows Driver внутри тоже ходит в 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-автотесты?
Начните примерно с десяти дымовых тестов. UI-тесты на порядки медленнее и хрупче модульных: вычисления и бизнес-правила закрывайте модульными тестами, доступ к БД — интеграционными, а UI оставьте проверке «приложение стартует и основные сценарии проходят». Если автоматизировать топ-10 сценариев, которые раньше гоняли руками перед релизом, написание займёт 1–2 недели, ночной прогон — 20–30 минут. План «все экраны, все поля» почти наверняка развалится. Если кажется, что UI-тестов нужно слишком много, это сигнал вынести логику в ViewModel и закрыть её модульными тестами.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог