AUTOMATION & PERFORMANCE

Automating a Legacy WPF Desktop App: What We Learned Choosing FlaUI

Most test automation content is about web apps. Automating a large, legacy Windows desktop application is a much less written-about problem — here's what it actually looks like.

Desktop automation quality is capped by how the application was built — not by your test framework.

Web automation tools assume a DOM: a structured, inspectable tree of elements with IDs and classes you can reliably select. A mature WPF application doesn't give you that for free. Instead, you're working with the Windows UI Automation framework, and whether an element is reliably identifiable depends entirely on whether the original developers added proper AutomationId and Name properties — which, on an app built over years before automation was a priority, they often didn't.

A framework can't invent a stable selector for a button that has no name and no automation ID. A meaningful chunk of "automation work" on a legacy desktop app is actually application work.

Choosing a framework: why FlaUI

For a WPF app on Windows, the realistic options are Microsoft's own UI Automation libraries directly, or a wrapper library that makes them less painful to work with. We chose FlaUI (specifically FlaUI.Core + FlaUI.UIA3) over writing directly against Microsoft UI Automation or using an older tool like Coded UI, which Microsoft has deprecated. FlaUI gives a much more usable C# API on top of UIA3, active maintenance, and it plays well with NUnit.

The resulting stack: C#, Visual Studio, NUnit as the test runner, FlaUI.Core and FlaUI.UIA3 for driving the app, and Microsoft UI Automation underneath all of it.

The three rules that actually matter for maintainability

Before writing a single test, we set three hard rules, because it's easy to end up with an automation suite that's more fragile than doing the testing manually.

  1. No coordinate-based or image-matching automation, ever, unless there's genuinely no other option. Clicking at a fixed pixel or matching a screenshot works until a window resizes, a theme changes, or the app runs on a different monitor DPI — then it silently breaks.
  2. No Thread.Sleep. A legacy desktop app's timing is unpredictable: database queries, UI grid rendering, and the underlying environment (a Citrix or VM-hosted deployment, for example) all introduce variable delays. Condition-based waits — "wait until this element exists and is enabled," polled with a timeout — are the only approach that holds up.
  3. Page Object / Screen Object structure from day one. Tests should read like a business flow ("open the order screen, enter a customer, submit"), not a script full of raw selectors. Selectors and waits live in a Pages layer; the test files stay readable by anyone on the QA team.

The unglamorous part: fixing the app's UI, not just the tests

The single biggest realisation was that automation architecture includes going into the app's XAML and adding proper AutomationProperties.AutomationId and AutomationProperties.Name to controls that don't have them. Third-party UI component libraries are a common offender — many of their interactive controls render with an empty Name and AutomationId out of the box, which makes them technically clickable but not reliably identifiable once there's more than one on screen.

QA Tip Treat "controls have automation IDs" as part of a feature's definition of done, the same way "has been tested" is. Retrofitting IDs onto an existing app is real, ongoing work — but it's the highest-leverage thing you can do, and it usually improves accessibility (screen reader support) at the same time, since both rely on the same underlying properties.

Where UI automation should hand off to database checks

Not everything should be verified through the UI, even in a UI automation suite. If a screen shows "Saved successfully," that confirms the UI displayed a success message — it doesn't confirm the record actually landed correctly in the database, with the right values, in the right related tables. For a desktop app backed by SQL Server, pairing UI actions with direct database assertions catches a category of bug that UI-only checks miss entirely, and it's often faster than adding more UI navigation just to verify a value on screen.

Where this leaves the project

Progress here has been deliberately incremental: confirm the app launches and the framework can find the main window, build a small utility to recursively dump every element's name, automation ID, control type and class, and build outward from there — one screen, one Page Object, one real business flow at a time. Desktop automation on a legacy app is a slower start than web automation with a clean modern frontend, but the payoff — a suite that catches real regressions on a business-critical app that almost nobody else knows how to automate — is worth the setup cost.

← Back to all articles