Back to Devexpress

Automated UI Tests (UI Automation, Appium, Coded UI)

windowsforms-404045-build-an-application-ui-tests-ui-automation-appium-coded-ui.md

latest24.8 KB
Original Source

Automated UI Tests (UI Automation, Appium, Coded UI)

  • Dec 29, 2025
  • 11 minutes to read

User interface (UI) testing verifies that all visual elements of an application function correctly. UI tests can be carried out manually by human testers, or with the help of automated testing tools. Automated testing is faster, more reliable, and more cost-effective.

Microsoft Coded UI Test (CUIT) Framework

The Coded UI Test Framework is a solution from Microsoft that utilizes the controls’ Accessibility layer to record and run UI tests. The CUIT component is distributed through the Visual Studio Installer.

This solution was declared obsolete in Visual Studio 2019 and beyond. In Visual Studio 2022, you can still run Coded UI tests, but cannot record new tests. Newer IDE versions will drop CUIT support completely.

See Also:

DevExpress Coded UI Extension

DevExpress Coded UI is the extension of Microsoft Coded UI Tests tailored specifically for DevExpress-based applications. The difference between these solutions is that, unlike Microsoft CUIT, the DevExpress Coded UI Extension does not utilize Accessibility. The framework communicates with controls through a proprietary channel and uses helper classes declared in DevExpress controls.

Microsoft’s decision to terminate CUIT also affects the DevExpress Coded UI Extension. For newer projects, we recommend that you use Appium or UI Automation instead.

See Also:

Appium and UI Automation

Appium is an open-source tool that allows you to create automated UI tests for web, hybrid, iOS mobile, Android mobile, and Windows desktop platforms. To test Windows apps, you need to set up the WinAppDriver.

See Also:

Appium (and multiple other testing frameworks) utilizes UI Automation — Microsoft’s Accessibility framework for Windows. You can use this framework directly (without any 3rd party solutions involved) to write UI tests.

See Also:

The choice between Appium and UI Automation depends on your scenario and the complexity of your testing requirements. Appium can be easier to use, but it is also more limited since it does not implement all UIA capabilities. For example, Appium lets you use pattern members, but only properties, not methods.

Step Recorders and Manual Test Scripting

Most test automation platforms ship recorder tools. These tools track your actions at runtime (cursor movement, clicks, and keyboard key presses) and generate code that emulates these actions. The following blog post shows how you can use Appium step recorder with DevExpress controls: Moving from CodedUI to Appium.

Recorders allow you to write less code, but they can produce unstable tests and cause performance issues. For example, most test recorders enumerate all parents of a target UI element in the element selection code. As a result, a minor UI modification (such as adding a new Panel container) causes this selection code to fail.

To avoid potential issues and get a better understanding of how your tests function, we recommend that you write test scripts manually. For instance, instead of listing the entire hierarchy of element parents, you can choose which parent controls to check for the target UI element or obtain this element directly without accessing any of its parents.

How to Write Appium and UI Automation Tests

Important

The following examples require Appium.WebDriver v4.x.x.

Common Test Structure

Appium and UI Automation tests share a similar hierarchy of code blocks, each block decorated by an NUnit attribute.

A general implementation of Appium and UIA tests looks like the following:

csharp
using System;
using NUnit.Framework;

namespace VisualTests {
    [TestFixture]
    public class MyAppTests {
        [SetUp]
        public void Setup() {
            // Actions repeated before each test
        }
        [TearDown]
        public void Cleanup() {
            // Actions repeated after each test
        }
        [Test]
        public void Test1() {
            // Test #1
        }
        [Test]
        public void Test2() {
            // Test #2
        }
    }
}
vb
Imports System
Imports NUnit.Framework

Namespace VisualTests
    <TestFixture>
    Public Class MyAppTests
        <SetUp>
        Public Sub Setup()
            ' Actions repeated before each test
        End Sub
        <TearDown>
        Public Sub Cleanup()
            ' Actions repeated after each test
        End Sub
        <Test>
        Public Sub Test1()
            ' Test #1
        End Sub
        <Test>
        Public Sub Test2()
            ' Test #2
        End Sub
    End Class
End Namespace

“Inspect” Tool

To write a test for any UI element, you need to do the following:

  1. Obtain this element by ID or name.
  2. Check which patterns it supports, and utilize properties and methods of these patterns to emulate user actions.
  3. Call the Assert.AreEqual method to compare the actual and expected control states.

To get the element name and ID, and check its available pattern APIs, use Microsoft Inspect – a free tool included in the Windows SDK installation.

Tip

Manual inspection of UI elements also allows you to locate bad Accessibility names and other issues. To fix these issues, handle the DXAccessible.QueryAccessibleInfo event. See the following post for more information: Customize Accessibility Properties.

How To Write Appium Tests

  1. Enable Developer Mode in Windows Settings.
  2. Download, install, and run WinAppDriver.
  3. Turn on the global WindowsFormsSettings.UseUIAutomation property in the project that you need to test.
  4. Create a new “Unit Test Project” in Visual Studio.
  5. Install the “Appium.WebDriver” NuGet package.
  6. Create tests according to the Common Test Structure section. The code below illustrates an automated test sample.
csharp
using System;
using System.Windows.Forms;
using NUnit.Framework;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;

namespace AppiumTests {
    [TestFixture]
    public class EditorsDemoTests {
        WindowsDriver<WindowsElement> driver;
        string editorsDemoPath =
            @"C:\Work\2022.1\Demos.Win\EditorsDemos\CS\EditorsMainDemo\bin\Debug\EditorsMainDemo.exe";
        [SetUp]
        public void Setup() {
            AppiumOptions options = new AppiumOptions();
            options.AddAdditionalCapability("app", editorsDemoPath);
            driver = new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), options);
        }
        [TearDown]
        public void Cleanup() {
            driver.Close();
        }
        [Test]
        public void ProgressBarTest() {
            var form = driver.FindElementByAccessibilityId("RibbonMainForm");

            var progressBarAccordionItem =
                form.FindElementByAccessibilityId("accordionControl1").FindElementByName("Progress Bar");
            progressBarAccordionItem.Click();
            Assert.AreEqual("True", progressBarAccordionItem.GetAttribute("SelectionItem.IsSelected"));
            AccessibleStates itemStates =
                (AccessibleStates)int.Parse(progressBarAccordionItem.GetAttribute("LegacyState"));
            Assert.IsTrue(itemStates.HasFlag(AccessibleStates.Selected));

            form.FindElementByName("Position Management").Click();

            var minMaxComboBox = form.FindElementByAccessibilityId("comboBoxMaxMin");
            minMaxComboBox.Click();
            minMaxComboBox.SendKeys(
                OpenQA.Selenium.Keys.Down + OpenQA.Selenium.Keys.Down + OpenQA.Selenium.Keys.Enter);
            Assert.AreEqual("Min = 100; Max = 200", minMaxComboBox.Text);

            var progressBar = form.FindElementByAccessibilityId("progressBarSample2");
            Assert.AreEqual("100", progressBar.GetAttribute("RangeValue.Minimum"));
            Assert.AreEqual("200", progressBar.GetAttribute("RangeValue.Maximum"));
            Assert.AreEqual("100", progressBar.GetAttribute("RangeValue.Value"));
            Assert.AreEqual("0%", progressBar.Text);

            form.FindElementByName("Step!").Click();
            Assert.AreEqual("110", progressBar.GetAttribute("RangeValue.Value"));
            Assert.AreEqual("10%", progressBar.Text);
        }
    }
}
vb
Imports System
Imports System.Windows.Forms
Imports NUnit.Framework
Imports OpenQA.Selenium.Appium
Imports OpenQA.Selenium.Appium.Windows

Namespace AppiumTests
    <TestFixture>
    Public Class EditorsDemoTests
        Private driver As WindowsDriver(Of WindowsElement)
        Private editorsDemoPath As String = "C:\Work\2022.1\Demos.Win\EditorsDemos\CS\EditorsMainDemo\bin\Debug\EditorsMainDemo.exe"
        <SetUp>
        Public Sub Setup()
            Dim options As New AppiumOptions()
            options.AddAdditionalCapability("app", editorsDemoPath)
            driver = New WindowsDriver(Of WindowsElement)(New Uri("http://127.0.0.1:4723"), options)
        End Sub
        <TearDown>
        Public Sub Cleanup()
            driver.Close()
        End Sub
        <Test>
        Public Sub ProgressBarTest()
            Dim form = driver.FindElementByAccessibilityId("RibbonMainForm")

            Dim progressBarAccordionItem = form.FindElementByAccessibilityId("accordionControl1").FindElementByName("Progress Bar")
            progressBarAccordionItem.Click()
            Assert.AreEqual("True", progressBarAccordionItem.GetAttribute("SelectionItem.IsSelected"))
            Dim itemStates As AccessibleStates = CType(Integer.Parse(progressBarAccordionItem.GetAttribute("LegacyState")), AccessibleStates)
            Assert.IsTrue(itemStates.HasFlag(AccessibleStates.Selected))

            form.FindElementByName("Position Management").Click()

            Dim minMaxComboBox = form.FindElementByAccessibilityId("comboBoxMaxMin")
            minMaxComboBox.Click()
            minMaxComboBox.SendKeys(OpenQA.Selenium.Keys.Down + OpenQA.Selenium.Keys.Down + OpenQA.Selenium.Keys.Enter)
            Assert.AreEqual("Min = 100; Max = 200", minMaxComboBox.Text)

            Dim progressBar = form.FindElementByAccessibilityId("progressBarSample2")
            Assert.AreEqual("100", progressBar.GetAttribute("RangeValue.Minimum"))
            Assert.AreEqual("200", progressBar.GetAttribute("RangeValue.Maximum"))
            Assert.AreEqual("100", progressBar.GetAttribute("RangeValue.Value"))
            Assert.AreEqual("0%", progressBar.Text)

            form.FindElementByName("Step!").Click()
            Assert.AreEqual("110", progressBar.GetAttribute("RangeValue.Value"))
            Assert.AreEqual("10%", progressBar.Text)
        End Sub
    End Class
End Namespace
  • The code above locates required UI elements with the help of the FindElementByName and FindElementByAccessibilityId methods. To obtain an element name or ID, browse element properties in Inspect.

  • To emulate mouse clicks and key presses, call the Click() and SendKeys methods.

  • Use the UIElement.GetAttribute method to obtain values of pattern properties. These names are also visible in Inspect.

  • DevExpress context menus have no direct owners. As a result, their accessible objects are children of the desktop window rather than the application window. To access items in these menus, utilize the desktop window driver.

How To Write UI Automation Tests

  1. Turn on the global WindowsFormsSettings.UseUIAutomation property in the project that you need to test.
  2. Create a new “Unit Test Project” in Visual Studio.
  3. Include UIAutomationClient.dll and UIAutomationTypes.dll libraries in your project.
  4. Create tests according to the Common Test Structure section. The code below illustrates an automated test sample.
csharp
using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Automation;
using Microsoft.Test.Input;
using NUnit.Framework;

namespace UIAutomationTests {
    [TestFixture]
    public class OutlookInspiredTests {
        string path =
            @"C:\Work\2022.1\Demos.RealLife\DevExpress.OutlookInspiredApp\
            bin\Debug\DevExpress.OutlookInspiredApp.Win.exe";
        Process appProcess;
        [SetUp]
        public void Setup() {
            appProcess = Process.Start(path);
        }
        [TearDown]
        public void TearDown() {
            appProcess.Kill();
        }
        [Test]
        public void Test1() {
            AutomationElement form =
                AutomationElement.RootElement.FindFirstWithTimeout(TreeScope.Children, new PropertyCondition(
                    AutomationElement.AutomationIdProperty, "MainForm"), 10000);

            AutomationElement grid =
                form.FindFirstWithTimeout(TreeScope.Descendants, new PropertyCondition(
                    AutomationElement.AutomationIdProperty, "gridControl"), 5000);

            AutomationElement cell = FindCellByValue(grid, "FULL NAME", "Greta Sims");
            Mouse.MoveTo(cell.GetPoint());
            Mouse.DoubleClick(MouseButton.Left);

            AutomationElement detailForm = 
                form.FindFirstWithTimeout(TreeScope.Children, new PropertyCondition(
                    AutomationElement.AutomationIdProperty, "DetailForm"), 5000);

            AutomationElement jobTitleEdit =
                detailForm.FindFirstWithTimeout(TreeScope.Descendants, new PropertyCondition(
                    AutomationElement.AutomationIdProperty, "TitleTextEdit"));
            ((ValuePattern)jobTitleEdit.GetCurrentPattern(ValuePattern.Pattern)).SetValue("HR Head");

            AutomationElement department =
                detailForm.FindFirstWithTimeout(TreeScope.Descendants, new PropertyCondition(
                    AutomationElement.AutomationIdProperty, "DepartmentImageComboBoxEdit"));
            ((ExpandCollapsePattern)department.GetCurrentPattern(ExpandCollapsePattern.Pattern)).Expand();

            AutomationElement managementItem =
                detailForm.FindFirstWithTimeout(TreeScope.Descendants, new PropertyCondition(
                    AutomationElement.NameProperty, "Management"));
            ((InvokePattern)managementItem.GetCurrentPattern(InvokePattern.Pattern)).Invoke();

            AutomationElement saveClose =
                detailForm.FindFirstWithTimeout(TreeScope.Descendants, new PropertyCondition(
                    AutomationElement.NameProperty, "Save & Close"));
            ((InvokePattern)saveClose.GetCurrentPattern(InvokePattern.Pattern)).Invoke();

            AutomationElement jobTitle =
                form.FindFirstWithTimeout(TreeScope.Descendants, new PropertyCondition(
                    AutomationElement.AutomationIdProperty, "sliTitle"));
            Assert.AreEqual("HR Head", jobTitle.Current.Name);
        }

        AutomationElement FindCellByValue(AutomationElement grid, string columnName, string cellValue) {
            TablePattern tablePattern = (TablePattern)grid.GetCurrentPattern(TablePattern.Pattern);
            AutomationElement[] headers = tablePattern.Current.GetColumnHeaders();
            int columnIndex = -1;
            for(int i = 0; i < headers.Length - 1; i++)
                if(headers[i].Current.Name == columnName)
                    columnIndex = i;
            if(columnIndex == -1)
                return null;
            for(int i = 0; i < tablePattern.Current.RowCount; i++) {
                AutomationElement cell = tablePattern.GetItem(i, columnIndex);
                if(cell != null) {
                    ValuePattern valuePattern = (ValuePattern)cell.GetCurrentPattern(ValuePattern.Pattern);
                    if(valuePattern.Current.Value == cellValue) {
                        return cell;
                    }
                }
            }
            return null;
        }
    }

    public static class AutomationElementExtensions {
        public static System.Drawing.Point GetPoint(this AutomationElement @this) {
            System.Windows.Point windowsPoint = @this.GetClickablePoint();
            return new System.Drawing.Point(Convert.ToInt32(windowsPoint.X), Convert.ToInt32(windowsPoint.Y));
        }
        public static AutomationElement FindFirstWithTimeout(this AutomationElement @this,
        TreeScope scope, Condition condition, int timeoutMilliseconds = 1000) {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            do {
                var result = @this.FindFirst(scope, condition);
                if(result != null)
                    return result;
                Thread.Sleep(100);
            }
            while(stopwatch.ElapsedMilliseconds < timeoutMilliseconds);
            return null;
        }
    }
}
vb
Imports System
Imports System.Diagnostics
Imports System.Threading
Imports System.Windows.Automation
Imports Microsoft.Test.Input
Imports NUnit.Framework

Namespace UIAutomationTests
    <TestFixture>
    Public Class OutlookInspiredTests
        Private path As String =
            "C:\Work\2022.1\Demos.RealLife\DevExpress.OutlookInspiredApp\bin\Debug\DevExpress.OutlookInspiredApp.Win.exe"
        Private appProcess As Process
        <SetUp>
        Public Sub Setup()
            appProcess = Process.Start(path)
        End Sub
        <TearDown>
        Public Sub TearDown()
            appProcess.Kill()
        End Sub
        <Test>
        Public Sub Test1()
            Dim form As AutomationElement = AutomationElement.RootElement.FindFirstWithTimeout(TreeScope.Children, New PropertyCondition(AutomationElement.AutomationIdProperty, "MainForm"), 10000)

            Dim grid As AutomationElement = form.FindFirstWithTimeout(TreeScope.Descendants, New PropertyCondition(AutomationElement.AutomationIdProperty, "gridControl"), 5000)

            Dim cell As AutomationElement = FindCellByValue(grid, "FULL NAME", "Greta Sims")
            Mouse.MoveTo(cell.GetPoint())
            Mouse.DoubleClick(MouseButton.Left)

            Dim detailForm As AutomationElement = form.FindFirstWithTimeout(TreeScope.Children, New PropertyCondition(AutomationElement.AutomationIdProperty, "DetailForm"), 5000)

            Dim jobTitleEdit As AutomationElement = detailForm.FindFirstWithTimeout(TreeScope.Descendants, New PropertyCondition(AutomationElement.AutomationIdProperty, "TitleTextEdit"))
            CType(jobTitleEdit.GetCurrentPattern(ValuePattern.Pattern), ValuePattern).SetValue("HR Head")

            Dim department As AutomationElement = detailForm.FindFirstWithTimeout(TreeScope.Descendants, New PropertyCondition(AutomationElement.AutomationIdProperty, "DepartmentImageComboBoxEdit"))
            CType(department.GetCurrentPattern(ExpandCollapsePattern.Pattern), ExpandCollapsePattern).Expand()

            Dim managementItem As AutomationElement = detailForm.FindFirstWithTimeout(TreeScope.Descendants, New PropertyCondition(AutomationElement.NameProperty, "Management"))
            CType(managementItem.GetCurrentPattern(InvokePattern.Pattern), InvokePattern).Invoke()

            Dim saveClose As AutomationElement = detailForm.FindFirstWithTimeout(TreeScope.Descendants, New PropertyCondition(AutomationElement.NameProperty, "Save & Close"))
            CType(saveClose.GetCurrentPattern(InvokePattern.Pattern), InvokePattern).Invoke()

            Dim jobTitle As AutomationElement = form.FindFirstWithTimeout(TreeScope.Descendants, New PropertyCondition(AutomationElement.AutomationIdProperty, "sliTitle"))
            Assert.AreEqual("HR Head", jobTitle.Current.Name)
        End Sub

        Private Function FindCellByValue(ByVal grid As AutomationElement, ByVal columnName As String, ByVal cellValue As String) As AutomationElement
            Dim tablePattern As TablePattern = CType(grid.GetCurrentPattern(TablePattern.Pattern), TablePattern)
            Dim headers() As AutomationElement = tablePattern.Current.GetColumnHeaders()
            Dim columnIndex As Integer = -1
            For i As Integer = 0 To headers.Length - 2
                If headers(i).Current.Name = columnName Then
                    columnIndex = i
                End If
            Next i
            If columnIndex = -1 Then
                Return Nothing
            End If
            For i As Integer = 0 To tablePattern.Current.RowCount - 1
                Dim cell As AutomationElement = tablePattern.GetItem(i, columnIndex)
                If cell IsNot Nothing Then
                    Dim valuePattern As ValuePattern = CType(cell.GetCurrentPattern(ValuePattern.Pattern), ValuePattern)
                    If valuePattern.Current.Value = cellValue Then
                        Return cell
                    End If
                End If
            Next i
            Return Nothing
        End Function
    End Class

    Public Module AutomationElementExtensions
         _
        Public Function GetPoint(ByVal this As AutomationElement) As System.Drawing.Point
            Dim windowsPoint As System.Windows.Point = this.GetClickablePoint()
            Return New System.Drawing.Point(Convert.ToInt32(windowsPoint.X), Convert.ToInt32(windowsPoint.Y))
        End Function
         _
        Public Function FindFirstWithTimeout(ByVal this As AutomationElement, ByVal scope As TreeScope, ByVal condition As Condition, Optional ByVal timeoutMilliseconds As Integer = 1000) As AutomationElement
            Dim stopwatch As New Stopwatch()
            stopwatch.Start()
            Do
                Dim result = this.FindFirst(scope, condition)
                If result IsNot Nothing Then
                    Return result
                End If
                Thread.Sleep(100)
            Loop While stopwatch.ElapsedMilliseconds < timeoutMilliseconds
            Return Nothing
        End Function
    End Module
End Namespace
  • Similar to Appium tests, elements are retrieved by their names or IDs copied from Inspect. Use the AutomationElement.FindFirst method to find the required elements.

  • The custom FindFirstWithTimeout method extends FindFirst by adding a timeout threshold. This value specifies the time during which the script can retry to acquire an element when this element is not immediately available.

  • The Mouse class exposes methods that allow you to emulate mouse actions. This class is available once you install the “Microsoft.TestApi” NuGet package. You can use other means to emulate clicks and pointer movement.

  • Pattern methods (TablePattern.GetColumnHeaders(), ValuePattern.SetValue(), and others) allow you to quickly find a required element, set the new control value, perform a default control action (for example, a click), and more. As mentioned in the Appium and UI Automation section, these methods are not available in Appium.

  • To obtain context menu items, utilize RootElements and TreeScope.Descendants.

Example: Create UI Automation Tests (GitHub)

How to Create UI Automation Tests for a DevExpress-powered WinForms Application