Skip to main content
All docs
V23.2

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

  • 12 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.

Note

Scheduler, Rich Edit, PDF Viewer, and Spreadsheet controls do not currently support UI Automation.

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

Common Test Structure

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

TestFixture
Decorates a class that contains tests.
SetUp
A method decorated with this attribute is called every time a test is about to start.
TearDown
The opposite of the SetUp attribute; this attribute decorates a set of instructions performed each time a test finishes.
Test
Decorates a method that contains a test script.

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

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
        }
    }
}

“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.

Control properties exposed by the Inspect tool

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.
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);
        }
    }
}
  • 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.

    To access properties of the LegacyIAccessible pattern, use the “Legacy{PropertyName}” format:

    var value = progressBarAccordionItem.GetAttribute("LegacyState");
    

    Properties of other patterns are accessed in the “{PatternName}.{PropertyName}” format:

    var value = progressBar.GetAttribute("RangeValue.Maximum");
    
  • 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.

    AppiumOptions globalDriverOptions = new AppiumOptions();
    globalDriverOptions.AddAdditionalCapability("app", "Root");
    var globalDriver = new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), globalDriverOptions);
    var menuItem = globalDriver.FindElementByName("ItemName");
    

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.
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;
        }
    }
}
  • 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.

    AutomationElement menuItem = AutomationElement.RootElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "itemName"));
    ((InvokePattern)menuItem.GetCurrentPattern(InvokePattern.Pattern)).Invoke();
    

Example: Create UI Automation Tests (GitHub)

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