UI Automated Testing for Windows Desktop Apps — How UI Automation Works and Building Robust Tests with FlaUI
· Go Komura · Testing, UI Automation, FlaUI, WinForms, WPF, C#, .NET, Windows, CI/CD
“Every release, clicking through every screen by hand to verify things takes a full two days.” “We fixed something last month, and a different screen right next to it broke — the customer found it.” “The web team automated their regression tests with Selenium or Playwright, but the desktop app has never been touched.” We hear this kind of consultation often from teams that have long maintained WinForms/WPF business applications.
At the same time, we hear just as many stories of teams enthusiastically starting to write tests for every screen, only to abandon the effort a year later because they couldn’t keep up with the maintenance burden. Get the tool choice and the “how far do we go” line wrong, and UI automated testing ends up costing more than manual testing. But if you understand the underlying mechanism, design tests that don’t break easily, and limit the scope to smoke tests, it becomes an investment that turns “two days before every release” into “twenty unattended minutes every night.” This article works through the whole pattern we use on real engagements: the foundation — how UI Automation (UIA) works — the current state of the tooling (FlaUI / WinAppDriver / Appium), implementation with FlaUI, design for robustness, and unattended execution in CI.
1. The bottom line up front
- UI automated testing sits at the top of the test pyramid. It’s slow, fragile, and time-consuming to diagnose when it fails, so it is not a substitute for unit and integration tests. Guard logic in the layers below, and limit UI tests to smoke tests that confirm “the app launches and the main flows work” (Chapter 6).
- The foundation is Windows UI Automation (UIA). You find elements by walking the automation tree rooted at the desktop, identify them by properties such as AutomationId / Name, and operate them through control patterns (Invoke, Value, SelectionItem, and so on).12
- Before writing any code, check how the target app’s elements actually appear using inspect.exe or Accessibility Insights for Windows. inspect is a legacy tool bundled with the Windows SDK; Accessibility Insights is now the officially recommended tool.3
- Our recommended toolset is FlaUI + xUnit / NUnit. FlaUI is an MIT-licensed OSS library that thinly wraps UIA, supports both UIA2 and UIA3, and is still actively maintained.4
- WinAppDriver, once Microsoft’s official choice, has had no stable release since v1.2.1 in November 2020, and since the server itself is closed-source, the community can’t even fix it. We don’t recommend adopting it for new work (Chapter 3).5
- Roughly 80% of test robustness comes down to the convention your development team uses for assigning AutomationId. In WPF that’s
x:NameorAutomationProperties.AutomationId; in WinForms it’sName/AccessibleName. Ban searches that rely on display text (Name), coordinate-based clicks, andThread.Sleep— write tests with conditional waits (Retry) and the Page Object pattern instead (Chapters 4-5).67 - Unattended execution in CI requires an interactive desktop session. It doesn’t work with an agent configured as a service, and it breaks when the screen locks or an RDP session disconnects. The standard setup is a self-hosted runner configured with automatic sign-in (autologon) (Chapter 7).8
2. How it works — the UI Automation tree, properties, and patterns
2.1 The automation tree
UI automated testing tools don’t recognize the screen as an image. Windows has an official platform called UI Automation (UIA) that lets assistive technologies such as screen readers programmatically read and operate an app’s UI, and test automation rides on that same rail.1
In UIA, every open window and the controls inside it are exposed as a tree structure rooted at the desktop. Buttons, text boxes, grid rows — every one of them is an automation element in the tree. Besides the raw view that contains every element, the tree also has filtered views: a control view limited to elements that are actual controls to operate, and a content view limited to content-bearing elements.1 Test code basically walks the control view to find elements.
The key point here is that you can only operate what’s exposed in the tree, not what’s visually visible. Screens built from standard controls expose cleanly into the tree, but owner-drawn lists, drawing areas rendered by a graphics library, or parts of a third-party grid may appear as nothing more than “a single element” in the tree. What the tree exposes directly sets the upper bound of what UI automated testing can cover — which is exactly why checking the tree before writing code (section 2.3) is the first thing to do.
2.2 Properties and control patterns
Properties are the clues you use to identify the element you want in the tree. In practice, these four matter:
| Property | Contents | Role in testing |
|---|---|---|
| AutomationId | An identifier assigned by the developer; independent of language (locale) | The go-to search key. Should be unique among sibling elements 6 |
| Name | The display-derived name (e.g., a button’s label) | Easy for humans to read, but breaks when text changes or when the app is localized |
| ControlType | The kind of control — Button, Edit, ComboBox, etc. | Helps narrow the search |
| ClassName | The implementation class name (e.g., a WinForms class name) | Last resort; fragile against implementation changes |
AutomationId is officially defined as something that “should stay the same regardless of locale” and “should be unique among sibling elements” — it’s a property dedicated to letting UI automated tests find elements reliably across languages and versions.6 Put another way, testing the UI of an app that hasn’t assigned AutomationId means falling back on fragile clues like display text or tree position. That’s the subject of Chapter 5.
Control patterns are how you operate an element once you’ve found it. UIA exposes a control’s functional capabilities — “can be clicked,” “has a value,” “can be selected” — as a set of patterns independent of the control type. The documentation itself describes the relationship between control patterns and the UI as “analogous to the relationship between a COM object and its interfaces”: you query an element for which patterns it implements, then operate it through that pattern.2 If you’re familiar with COM, think of it as the UI equivalent of QueryInterface. The main patterns are:
| Pattern | What it does | Typical controls |
|---|---|---|
| Invoke | Executes the default action (equivalent to a click) | Buttons, menu items |
| Value | Gets/sets a value | Text boxes |
| SelectionItem / Selection | Selects items, retrieves selection state | Lists, combo boxes, tabs |
| Toggle | Toggles on/off | Checkboxes |
| ExpandCollapse | Expands/collapses | Combo boxes, tree items |
| Text | Reads text content | Documents, rich text |
| Window | Maximize/minimize/close | Top-level windows |
| Scroll / ScrollItem | Scrolling, bringing an item into view | Lists, grids |
Test code that “clicks a button” is, under the hood, “get that element’s Invoke pattern and call Invoke().” It doesn’t compute coordinates and send mouse events — it calls the operation the control itself exposes. That difference is what makes tests stable regardless of window position or DPI.
2.3 Confirming what’s “actually visible” with inspect.exe and Accessibility Insights
The best way to see what AutomationId / Name / ControlType / patterns the target app’s elements are actually exposing is to look at the real thing with a tool.
- inspect.exe: The classic tool bundled with the Windows SDK (found under
bin\<version>\<platform>in the SDK install directory). Select an element with the mouse or keyboard focus and it lists the UIA properties and patterns, and lets you verify tree navigation too. That said, it’s officially positioned as a legacy tool, and migrating to Accessibility Insights is recommended.3 - Accessibility Insights for Windows: Microsoft’s current recommended tool. Its Live Inspect feature — where you can check an element’s UIA properties just by hovering the mouse or moving focus — is convenient, and it also comes with automated accessibility checks (FastPass).3
- FlaUInspect: The inspector bundled with the FlaUI project. It lets you view the tree from the UIA2 and UIA3 perspectives that FlaUI actually uses, so it’s worth installing alongside FlaUI if you’re going to write tests with it.4
As a rule of thumb, the first thing to do when introducing UI automated testing isn’t “write test code” — it’s open the main screens in an inspect-style tool and take stock of how much AutomationId coverage already exists. If that turns up sparse, it’s ultimately faster to start with app-side remediation (Chapter 5) first.
3. Tool options — why we recommend FlaUI, and where WinAppDriver stands today
You can hit UIA directly via COM, but in practice you use a wrapper library. Here’s where the options stand as of 2026, based on facts we verified ourselves.
| Tool | Form | Status (as of 2026) | Guidance for new adoption |
|---|---|---|---|
| FlaUI | .NET library (MIT) | Actively maintained OSS. v5.0.0 released February 2025 4 | Top choice |
| WinAppDriver | WebDriver-protocol server (Microsoft) | Last stable release v1.2.1 was November 2020. v1.3 remains a July 2020 RC. Over 1,100 open issues 5 | Effectively stalled. Avoid for new work |
| Appium Windows Driver | Appium’s Windows driver | Uses WinAppDriver internally, so it inherits the same constraints 9 | Only if you already have Appium assets |
| Coded UI Tests | Visual Studio feature | Deprecated in VS 2019, removed in VS 2026 10 | Migration target |
3.1 FlaUI — currently the most practical thin wrapper over UIA
FlaUI is a .NET library that supports UI automated testing for Windows apps (Win32 / WinForms / WPF / Store apps), designed as a wrapper around Microsoft’s native UIA libraries.4 The package layout splits the shared parts into FlaUI.Core, with FlaUI.UIA2 / FlaUI.UIA3 chosen depending on which UIA implementation you want to use.
FlaUI’s FAQ spells out how to choose between UIA2 and UIA3: UIA2 is a managed-only implementation that doesn’t support newer features like touch and works less well with WPF or Store apps; UIA3 is the newer implementation, ideal for WPF/Store apps, but can hit issues in WinForms apps that UIA2 doesn’t have.4 In other words, the practical rule of thumb is: try UIA3 for WPF and UIA2 for WinForms, and use whichever proves stable against your actual app. Being able to pull in both from NuGet and switch between them is one of the nice things about FlaUI’s being a thin wrapper.
FlaUI doesn’t include its own test framework, so you combine it with xUnit / NUnit / MSTest and write it as an ordinary test project. Running on the same test runner and the same CI pipeline as your unit tests pays off considerably in day-to-day operations.
3.2 WinAppDriver — honestly, it’s effectively stalled
WinAppDriver is a Microsoft UI test server that lets you drive Windows apps through the same WebDriver protocol used by Selenium, and for a while it was treated as the default choice. When Visual Studio’s Coded UI Tests were deprecated, Microsoft itself pointed users toward “Selenium for web, Appium + WinAppDriver for desktop and UWP” as the migration path.10
But checking GitHub today shows that the last stable release, v1.2.1, shipped in November 2020, with no stable release since. v1.3 remains stuck as a July 2020 Release Candidate (v1.2.99) that never went final, and there are now more than 1,100 open issues.5 What’s more troubling is that the GitHub repository contains only documentation, samples, and the issue tracker — the source code for the server itself was never published. That means the community can’t even fix bugs in it. Our judgment is that “it’s official, so it’s safe” is not a good enough reason to adopt it fresh in 2026. If you already have test assets running on WinAppDriver, there’s no need to throw them away immediately, but stop expanding on it, and steer new test flows toward FlaUI instead.
3.3 Appium Windows Driver and other options
Appium Windows Driver (appium-windows-driver) is Appium’s driver for operating Windows apps, but its actual implementation is “an interface to the WinAppDriver provided by Microsoft,” with all the heavy lifting done by WinAppDriver itself.9 It therefore inherits WinAppDriver’s stagnation wholesale. It’s still an option for teams that have standardized on Appium across mobile and web, or that want to write test code in Java or Python — but even then, adopt it only after understanding the constraints and limited future it inherits from WinAppDriver.
WinUI 3 (Windows App SDK) apps also support UIA, so they can be tested with FlaUI (UIA3). That said, there’s less accumulated case experience compared to WinForms/WPF, which makes upfront verification with inspect-style tools even more important, given the quirks in how individual controls are exposed. For the choice of UI framework itself, see “How to Choose Between WinForms, WPF, and WinUI — A Practical Decision Table.”
4. A minimal FlaUI implementation — launch, find, operate, verify
Enough theory — let’s look at working code. This is an ordinary test project with FlaUI.UIA3 (or FlaUI.UIA2 if WinForms proves unstable) and xUnit pulled in from NuGet.
4.1 The basic shape of a smoke test
A test for the flow “launch the app, open the order-entry dialog, save one record, and confirm the result appears in the status bar” looks like this:
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
{
// Waits until the main window appears
var window = app.GetMainWindow(automation);
// Find the element by AutomationId and operate it as a Button
window.FindFirstDescendant(cf => cf.ByAutomationId("NewOrderButton"))
?.AsButton().Invoke();
// The dialog opens asynchronously, so wait conditionally for it to appear
var dialog = Retry.WhileNull(
() => window.FindFirstDescendant(
cf => cf.ByAutomationId("OrderDialog"))?.AsWindow(),
timeout: TimeSpan.FromSeconds(5)).Result;
Assert.NotNull(dialog);
// Enter text via the Value pattern (not keyboard emulation)
dialog.FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox"))
.AsTextBox().Text = "Test Trading Co.";
dialog.FindFirstDescendant(cf => cf.ByAutomationId("SaveButton"))
.AsButton().Invoke();
// Verify save completion via a conditional wait on the status label too
var saved = Retry.WhileFalse(
() => window.FindFirstDescendant(
cf => cf.ByAutomationId("StatusLabel"))
?.Name.Contains("Saved") == true,
timeout: TimeSpan.FromSeconds(10));
Assert.True(saved.Success);
}
finally
{
app.Close();
}
}
}
There are only four building blocks here.
- Launch:
Application.Launchstarts the process (you can also attach to an already-running app withApplication.Attach).GetMainWindowwaits until the main window becomes available. - Find:
FindFirstDescendant(cf => cf.ByAutomationId(...))is the tree search.cfis a condition factory, and you can also composeByName/ByControlType/Andconditions. As noted above, AutomationId should be your go-to search key. - Operate: Convert the found element to a typed wrapper such as
AsButton()/AsTextBox()/AsComboBox()and operate it.Invoke()is the Invoke pattern, and theTextproperty is the Value pattern — the control patterns from section 2.2 sit directly behind these calls. - Verify: Assert on the results that show up in the UI (labels, row counts, window titles, and so on). If you want to check the database contents directly, there’s nothing wrong with reading the DB straight from the test code.
4.2 Wait with Retry — writing Sleep is a loss
The single biggest source of flakiness in UI tests is timing. Until a dialog opens, until data finishes loading, until a button becomes enabled — the UI is always changing asynchronously, and if the test code assumes “it must already be showing by now,” you end up with a test that only fails on slower machines.
That doesn’t mean inserting Thread.Sleep(3000) is a good fix — it’s the worst one. On a fast machine you waste time waiting for nothing; on a slow machine it isn’t enough and the test fails anyway; and across the whole suite, total execution time just keeps piling up. The answer is a conditional wait — poll until the condition is satisfied, and give up on timeout — and FlaUI provides a dedicated Retry class for exactly this.4
// Wait up to 5 seconds for it to stop being null (i.e., the element appears)
var element = Retry.WhileNull(
() => window.FindFirstDescendant(cf => cf.ByAutomationId("ResultGrid")),
timeout: TimeSpan.FromSeconds(5),
interval: TimeSpan.FromMilliseconds(200),
throwOnTimeout: true).Result;
// Wait until the condition becomes true (i.e., the button becomes enabled)
Retry.WhileFalse(
() => saveButton.IsEnabled,
timeout: TimeSpan.FromSeconds(5),
throwOnTimeout: true);
Retry.WhileNull / WhileFalse / WhileTrue / WhileException and similar helpers are provided, and you can specify the timeout, polling interval, and whether to throw an exception on timeout.4 One thing worth noting: since version 2.0, FlaUI has dropped implicit retries on its Find methods, adopting the policy that “where to wait is something the test code states explicitly.”4 FindFirstDescendant only looks at “the tree at this exact instant,” so make it a rule that any search for an element that appears asynchronously must be wrapped in Retry. This idea — building an explicit wait condition instead of papering over timing with Sleep — is a general rule of thumb for Windows programming as a whole, not just UI testing (see “Why You Should Prefer Event Waits over Sleep(1) on Windows”).
5. Design for robustness — conventions on both the app side and the test side
The reasons UI tests get “abandoned because the maintenance burden was too much” boil down to essentially four things: searches that depend on display text, coordinate-based clicks, Sleep, and a lack of shared structure. We’ll kill each one with a convention.
5.1 Always assign AutomationId on the app side
This is the most important item of all. Test robustness is determined, before anything about how the test code is written, by whether the app exposes stable identifiers. AutomationId is precisely the property meant for this: the spec requires it to be locale-independent and unique among sibling elements.6
In WPF, an element given x:Name gets that name used as its UIA-side identifier, so any control that’s already named is testable with no extra work. If you want to set one explicitly — inside a data template, for example — set the attached property AutomationProperties.AutomationId.67
<!-- x:Name becomes the identifier as-is -->
<Button x:Name="SaveButton" Content="Save" Click="OnSave" />
<!-- Set AutomationProperties.AutomationId explicitly for things like template content -->
<Button AutomationProperties.AutomationId="DeleteRowButton"
Content="Delete"
Command="{Binding DeleteCommand}" />
In WinForms, Control.Name — the name you set in the designer (the one you change from button1 to saveButton) — is used for UIA-side identification. In our experience, forms where Name has been assigned per convention can usually be searched by AutomationId as-is, but since the appearance varies by framework generation and control type, always confirm the actual AutomationId with an inspect-style tool before using it as a search key in a test. Also note that the UIA Name property, which becomes the name read aloud for screen readers, is derived from the Text property for many controls, but for control types like TextBox or ListView that don’t reuse Text, you need to set AccessibleName explicitly.11 The fact that setting up AutomationId isn’t just for testing but is exactly the same work as accessibility support is a good argument to bring when securing budget internally.
The convention itself can be simple. We add these two lines to our clients’ coding standards:
- Any control placed on a screen that could plausibly be operated on or verified against should be given a meaningful
Name(WinForms) /x:NameorAutomationProperties.AutomationId(WPF) - Once an identifier is referenced from a test, any rename must happen together with the test side (treat the identifier as a public API)
5.2 Ban coordinate-based clicks
Operations of the form “click screen coordinates (830, 412)” break the moment window position, resolution, DPI scaling, theme, or font settings change. DPI in particular produces “passes locally, fails in CI” failures en masse, given environment differences like a dev machine at 100% versus a CI machine at 150% (see “High-DPI Support in WinForms” for how DPI works). As we saw in Chapter 2, operating through UIA control patterns is coordinate-independent. FlaUI does have APIs for direct mouse operation, but decide up front that they’re to be used only for operations that can’t be expressed through a pattern — drag-and-drop, or a drawing canvas, for example. And even then, compute the relative position from the element’s BoundingRectangle rather than from screen coordinates.
5.3 Consolidate structure in one place with the Page Object pattern
If you hardcode search code like FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox")) directly into test bodies, you end up having to fix every test whenever the screen layout changes. The standard fix is the Page Object pattern: create one class per screen (or dialog), and enclose element search and operation logic inside it.
public sealed class OrderDialogPage
{
private readonly Window _dialog;
public OrderDialogPage(Window dialog) => _dialog = dialog;
// Element search code lives only inside this class
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();
// Tests only see operations expressed in business terms
public void Register(string customerName)
{
CustomerName.Text = customerName;
Save.Invoke();
// Do the element search and null check inside the retry too. On screens
// where the status label is created/redrawn after saving, it's normal
// for the search to briefly return null or throw (without ignoreException,
// the first exception would fail the call outright)
Retry.WhileFalse(() => Status?.Name.Contains("Saved") == true,
timeout: TimeSpan.FromSeconds(10), throwOnTimeout: true,
ignoreException: true);
}
}
The test body can now be written in business terms as new OrderDialogPage(dialog).Register("Test Trading Co."), and the impact of a screen change is confined to a single fix in the Page Object. If you’re going to keep running UI tests on an app with more than about 10 screens, this pattern is effectively mandatory. One caveat: for any element referenced from a wait condition, always re-search it via a property each time, as with Status above. If you cache the search result in a field, you’ll end up holding onto a stale element that disappeared on redraw and waiting all the way to the timeout.
5.4 Test independence — don’t carry state between tests
Keep each UI test independent. If you create order dependencies like “test 3 assumes data created by test 2,” a single failure cascades and you also lose the ability to reorder tests. The principles are:
- Each test (or test class) launches the app itself, and reliably closes it when done (via
finallyor anIDisposablefixture) - The test itself sets up any prerequisite data. Giving the app a launch option that lets tests swap in test-specific config files/databases makes this dramatically easier
- Don’t forget cleanup on failure. Leftover processes or stray modal dialogs from a previous failure become the cause of the next test’s failure (Chapter 7)
6. How far to cover with UI tests — a smoke-test-centered line
Once the tooling is in place, it’s tempting to want to test every screen — but this is the watershed moment. UI tests are orders of magnitude slower than unit tests (seconds to tens of seconds each), more fragile (they need to be kept up to date with every UI change), and slower to diagnose on failure (is it an app bug, a test bug, or the environment?). This cost structure is exactly why the top of the test pyramid is drawn narrow — using a UI test to guard something the layers below can already guard is always a net loss.
Put into a decision table, it looks like this:
| What you want to guard | Appropriate layer | Reason |
|---|---|---|
| Calculations, conversions, business rules | Unit tests | Fast and stable. Covering these via the UI is a non-starter |
| DB access, file I/O, external integration | Integration tests | Use the real thing without needing the UI (see where to draw the boundary) |
| ViewModel / presentation logic | Unit tests | With MVVM, testable without the UI |
| Launches, main flow works, can save | UI smoke test | This is the main battleground for UI testing |
| Critical flows previously missed by manual checks | UI test (regression) | Add only where real harm occurred |
| Screen layout / visual breakage | Visual inspection / screenshot comparison | Writing this as assertions is maintenance hell — keep the count small |
| Crashes, handle leaks, and other abnormal conditions | A different foundation | Outside the scope of UI testing (see Application Verifier) |
The way we recommend starting is with “around 10 smoke tests”: “it launches and the main screen appears,” “the key master screens can be opened,” “a representative document can be registered, searched for, and print-previewed,” “no error appears on exit.” Automate the top 10 flows you were previously always confirming by hand before every release. At this scale, writing them takes only one or two weeks, nightly runs fit within 20-30 minutes, and the maintenance load stays realistic. Once you feel the benefit, add regression tests one at a time for regression bugs that actually caused real harm. A plan aimed at “every screen, every field,” on the other hand, will almost certainly collapse partway through.
Another important direction of investment is pulling logic out of the UI instead of adding more UI tests. A screen where business logic is written directly inside event handlers can only be guarded by a UI test, but if you move that logic into a ViewModel or a service class, it becomes guardable by unit tests, and the UI test only needs to confirm the “wiring.” If you find yourself needing a large number of UI tests, our experience is that it’s often a design problem rather than a testing problem. For how to layer tests overall, see “How to Draw the Boundary Between Unit Tests and Integration Tests,” and for the practicalities of lower-layer tests, see “Minimum Requirements for a Custom Logger and an Integration Test Checklist.”
7. CI and unattended-execution pitfalls — UI tests don’t run without a desktop
Running the UI tests you’ve written by hand on your own machine is peaceful. The pitfalls concentrate at the “running unattended every night in CI” stage, and unlike web UI tests (which run to completion in a headless browser), the root of everything is that desktop app tests demand an actual, interactive desktop session.
7.1 An interactive session is required — it won’t run on an agent started as a service
CI agents (Azure Pipelines agents, GitHub Actions self-hosted runners, etc.) are normally kept resident as Windows services. But a service has no user desktop, so you can’t operate windows from an app launched from one. Azure Pipelines’ own documentation states explicitly that an agent running UI tests for a desktop app must be configured as an interactive process with automatic sign-in (autologon) enabled — not as a service.8 It also notes that Microsoft-hosted agents (the cloud-provided shared runners) don’t support tests with visible UI at all — only headless-browser tests run there.8 In other words, a self-hosted machine (physical or VM) is effectively mandatory for UI testing of a desktop app.
The officially documented security risk of an autologon configuration is that “anyone with physical access to that machine can use the automatically signed-in account.”8 The premise is a dedicated test account on a dedicated machine (VM), with no production credentials stored on it.
7.2 Screen locks, RDP disconnects, resolution — the classic ways it breaks
Even once you’ve set up an interactive session, there are still pitfalls. Here’s a table of symptoms and countermeasures.
| Pitfall | Symptom | Countermeasure |
|---|---|---|
| Agent started as a service | No elements found at all, or the app never launches | Reconfigure as an interactive process + autologon 8 |
| Screen lock / screensaver | Input operations don’t reach the app and fail | Disable the screensaver as part of the autologon setup. Request a GPO exemption for policies that trigger a lock 8 |
| Disconnecting RDP via the “X” button | The session locks the instant you disconnect, and every subsequent test fails | Run tscon <session ID> /dest:console to move the session back to the console before disconnecting 8 |
| Environment differences in resolution/DPI | Tests that pass locally fail only in CI | Pin the resolution (Azure Pipelines has a task for this). Standardize scaling at 100% 8 |
| Running tests in parallel | Contention over mouse/keyboard/focus corrupts each other | Run UI tests one at a time, serially, per machine. Parallelize by adding more machines (VMs) instead |
| Leftovers from a previous failure | A stray process or modal dialog blocks the next launch | Clean up target processes before starting the test. Always run teardown in a finally |
The RDP pitfall is especially easy to step in, so a bit more detail: if you remote into the test machine to make adjustments, close the window, and log out, that session ends up locked, and every subsequent UI test keeps failing. The workaround the official documentation recommends is to run %windir%\System32\tscon.exe <ID> /dest:console from an administrator command prompt before disconnecting, to return the session to the console.8 Make sure this is written into your test machine’s operating runbook.
DPI and resolution also deserve caution. It’s not unusual for a CI VM to be left at 1024x768 with default scaling, which changes the layout — you can end up with differences like “a button visible on the dev machine now requires scrolling to see.” Eliminating coordinate-based clicks absorbs most of this, but it’s still better to pin the environment. The same checkpoints from “High-DPI Support in WinForms” apply directly here, including the app’s own DPI-awareness status.
7.3 Preserve evidence on failure — screenshots and logs
When an unattended UI test fails, a log that just says “element not found” tells you nothing about the cause. Build in saving a screenshot on failure from the start. FlaUI has functionality for capturing the screen or an element, so call it from your test framework’s failure hook and save it as a CI artifact.
// Call this from a failure hook or similar. Output to the CI artifact directory.
FlaUI.Core.Capturing.Capture.Screen()
.ToFile(Path.Combine(artifactDir, $"{testName}_{DateTime.Now:HHmmss}.png"));
On top of screenshots, make sure the app’s own log (how far processing got) and the test’s log (which operations succeeded) can be cross-referenced by timestamp — that makes separating “app bug, test bug, or environment” dramatically faster. For notes on running tests overnight via Task Scheduler (session types, tasks ending with 0x1, and so on), see “When Task Scheduler Tasks Don’t Run, or Exit with 0x1.”
8. Summary
UI automated testing isn’t something that “just works once you install a tool” — it only keeps running once you have an understanding of the mechanism, cooperation on the app side, a clear line on scope, and a well-designed execution environment, all together. To compress the key points:
- The foundation is UI Automation. Find elements in the tree by AutomationId, operate them through control patterns. Start by taking stock of how the target app looks with inspect / Accessibility Insights
- The tooling is FlaUI + xUnit / NUnit. WinAppDriver hasn’t had a stable release since 2020 — avoid it for new adoption
- Robustness is built through convention. Assign AutomationId on the app side, ban coordinate clicks, ban Sleep in favor of conditional waits via Retry, and consolidate structure with Page Object
- Scope from around 10 smoke tests. Push logic down into unit and integration tests, and keep UI tests focused purely on confirming “the main flow works”
- CI’s basic form is a self-hosted machine + an interactive session + autologon. Close off the pitfalls of screen locks, RDP disconnects, and resolution differences through operational procedure, and always keep a screenshot on failure
“Two days of manual verification before every release” can realistically be replaced, at a reasonable cost, by properly scoped UI automated tests. Conversely, if your app currently has no AutomationId assigned anywhere, or logic is written directly into screen events, it’s ultimately faster to start with small app-side fixes before writing any tests at all. We can help from the initial survey of your app’s current state onward, including where to start and how to stand up a smoke-test suite.
Related Articles
- How to Draw the Boundary Between Unit Tests and Integration Tests
- Building a Windows Abnormal-Condition Test Foundation with Application Verifier
- Minimum Requirements for a Custom Logger and an Integration Test Checklist
- Maintaining PowerShell Tests with Pester — A Practical Pattern for Making Operational Scripts Harder to Break
- High-DPI Support in WinForms — Why Things Blur or Break on 4K Monitors, and Practical Fixes
Related Consulting Areas
Komura Soft LLC handles introducing UI automated testing for WinForms/WPF applications (current-state assessment, AutomationId remediation, building out a full smoke-test suite, CI environment design), migrating existing test assets to FlaUI, and design reviews of overall test strategy.
- Technical Consulting / Design Review
- Windows App Development
- Legacy Asset Reuse / Migration Support
- Contact Us
References
-
Microsoft Learn, UI Automation Overview. On the automation tree rooted at the desktop, the raw / control / content views, element properties and control patterns, and how assistive technology and test automation share the same foundation. ↩ ↩2 ↩3
-
Microsoft Learn, UI Automation Control Patterns Overview. On the design of control patterns (the analogy to COM interfaces), patterns such as Invoke / Value / SelectionItem, and how a single control can implement multiple patterns. ↩ ↩2
-
Microsoft Learn, Accessibility tools - Inspect. On inspect.exe being bundled with the Windows SDK and able to show UIA properties and patterns, and on it being positioned as a legacy tool with Accessibility Insights recommended instead. ↩ ↩2 ↩3
-
GitHub, FlaUI/FlaUI. On it being a UIA wrapper supporting Win32 / WinForms / WPF / Store apps, the FlaUI.Core / UIA2 / UIA3 package layout, the UIA2-vs-UIA3 guidance in the FAQ, its MIT license, the v5.0.0 release (February 2025) and ongoing development, the Retry utility, and the removal of implicit retries on Find methods since 2.0. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
GitHub, microsoft/WinAppDriver. On the last stable release v1.2.1 shipping in November 2020, v1.3 remaining an unreleased July 2020 Release Candidate, more than 1,100 open issues, and the repository containing mainly documentation and samples with the server’s own source code unpublished. ↩ ↩2 ↩3
-
Microsoft Learn, Use the AutomationID Property. On AutomationId being a locale-independent identifier, needing to be unique among sibling elements, its use as a search key in test scripts, and how in WPF, controls without an ID (x:Name) or x:Uid don’t support AutomationId. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, AutomationProperties.AutomationId Attached Property. On the attached property definition, in the System.Windows.Automation namespace, for setting a string that uniquely identifies an element in WPF. ↩ ↩2
-
Microsoft Learn, UI testing considerations (Azure Pipelines). On desktop app UI testing requiring an agent configured as an interactive process with autologon enabled, Microsoft-hosted agents not supporting visible UI tests, the lock caused by an RDP disconnect and the tscon workaround, the screen-resolution configuration task, and collecting screenshots/video on failure. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
GitHub, appium/appium-windows-driver. On Appium Windows Driver being an interface to Microsoft’s WinAppDriver, and the README’s warning that the WinAppDriver server has gone without maintenance for a long period. ↩ ↩2
-
Microsoft Learn, Use Coded UI tests to test your code. On Coded UI Tests being deprecated, with Visual Studio 2019 being the last version with full support, and on Appium + WinAppDriver having been the recommended migration path for desktop / UWP apps (removal in VS 2026 is documented in the same Learn migration guide). ↩ ↩2
-
Microsoft Learn, WinForms: Setting the accessible name on a control. On how the UIA Name property for many WinForms control types is derived from the Text property, and how controls such as TextBox / ListBox require AccessibleName to be set explicitly. ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Integrating Entra ID Authentication into WinForms/WPF Apps — A Practical Architecture with MSAL.NET and the WAM Broker
A practical, hands-on look at integrating Entra ID (formerly Azure AD) authentication into WinForms/WPF desktop apps: the public client m...
Windows App Outsourcing and Contract Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or contract development, here is how to sort out existing software modification, device inte...
Date, Time, and Timezones in Business Apps — From DateTime Pitfalls to the UTC-Storage Principle and Test Design
Timestamps drift by nine hours after a server migration; only the overseas office's dates roll back to the previous day — we trace date/t...
WPF High-DPI Support — Why It Still Blurs and Bleeds Despite Being 'Supposedly DPI-Aware,' and How to Fix It
WPF lays out UI in DIPs (1/96 inch) and is System DPI Aware from the start, but moving a window to a monitor with a different DPI blurs t...
High-DPI Support in WinForms — Why the UI Blurs or Breaks on 4K Monitors, and Practical Fixes
This article organizes the reasons WinForms apps blur or have broken layouts on 4K monitors and 150% scaling, starting from DPI virtualiz...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
UI Threading & Timers
Topic page for WPF / WinForms UI threading, async flow, Dispatcher usage, and timer decisions.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links