Enhance WinForms Application Reliability with UI Test Automation

WinForms Team Blog
04 March 2024

As you know, UI Automation testing leverages specific tools/frameworks to simulate user interactions with the interface - and helps ensure an app meets relevant end-user requirements. When used in conjunction with other testing methods (API tests, Unit tests, etc), UI automation can improve application stability, reduce time spent on manual testing, and of course improve user satisfaction. In this blog post I’ll show you how to write simple/advanced UI tests in Visual Studio 2022 using UI Automation.  

App Testing Methodologies

Before I start - a word or two about the advantages of UI testing:

  • UI tests target the application and allow you to test application flow (end-to-end testing), covering all elements of the application including both the UI and business logic (while unit tests focus on testing individual modules, classes, or components within the application).
  • UI tests help identify issues related to navigation, data input, and workflow across different screens that may not be caught by other tests.
  • UI tests offer efficiency and scalability for testing complex scenarios and edge cases (unit tests are essential for testing individual units of code). Note that UI tests may take longer to execute since they interact with the UI and run later in the application development pipeline (unit tests are typically faster and checked before committing to the repository).

How UI Test Automation Works

UI tests do not have direct access to real app objects. The obvious question is this - without such access, how do tests interact with UI controls? The answer is that the Windows Forms platform allows you to interact with the application through Automation Elements (the Windows Forms platform builds an automation tree that can be queried by external applications). Each element in the automation tree contains information about the UI element and can be used to perform basic actions (such as click, read/change text, scroll, select), all available through automation patterns. UI tests access specific automation elements and interact with them as needed.

The Accessibility Insights tool allows you to inspect the automation tree built by a specific UI control and view UI automation control patterns.

Accessibility Insights - DevExpress WinForms Form

Assistive technologies (such as Narrator and NVDA) rely on the automation tree and structure of UI elements as well. With our v23.2 release, we enhanced the accessibility tree for most DevExpress WinForms UI controls to ensure assistive technologies can obtain the necessary information to comply with accessibility guidelines outlined in WCAG.

Create UI Automation Tests

1. Configure WinForms Application for Testing

The WinForms application ("UIAutomationTestingExample") I'll test includes the following data forms:

  • LogInForm - simulates a call to an authorization service that asynchronously returns a user's login result (with a delay).

    Log In Form
  • CustomersForm – Includes a DevExpress Data Grid used to display customer information on-screen. The "Name" column displays customer names from a data source. The "Is Modified" unbound column indicates whether the end-user modified customer information.

    Customers Form

To begin, I'll enable the WindowsFormsSettings.UseUIAutomation setting at application startup to force DevExpress UI components to use UI Automation patterns and create a full UI Automation tree:

using System;
using System.Windows.Forms;
using DevExpress.Utils;
using DevExpress.XtraEditors;

namespace UIAutomationTestingExample {
    internal static class Program {
        /// 
        /// The main entry point for the application.
        /// 
        [STAThread]
        static void Main() {
            WindowsFormsSettings.UseUIAutomation = DefaultBoolean.True;

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            LogInForm loginForm = new LogInForm();
            if (loginForm.ShowDialog() == DialogResult.OK) {
                CustomersForm customersForm = new CustomersForm();
                Application.Run(customersForm);
            }
        }
    }
}

2. Create NUnit Test Project

Next, I'll create a project (TestRunner) that contains NUnit tests and add it to the solution.

Create NUnit Test Project

I'll have to reference UIAutomationClient and UIAutomationTypes assemblies. These assemblies contain classes needed for Automation Elements.

Reference Automation Elements Assemblies

3. Create a Test for the LogIn Form

Before moving on to the test itself, I want to clarify a couple of points:

  • The AutomationElement.RootElement static property contains the root element. Use this property to access your application.

  • The AutomationElement.FindFirst method allows you to find a specific UI element:

    
            AutomationElement logInFormElement = AutomationElement.RootElement.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "CRM Log In Form")); 
    		

    The FindFirst method gets two parameters. The first parameter (scope) specifies the scope of the search. The second parameter (condition) specifies the criteria to match (in my case this is a form with AutomationName = "CRM Log In Form").

    UI controls can automatically 'calculate' AutomationName based on other settings (for example, Text).
    If necessary, you can explicitly set the AutomationName property or handle the DXAccessible.QueryAccessibleInfo event to supply accessibility information to DevExpress UI elements.
  • In some instances, the application under test may not have time to generate UI elements because the UI Automation framework performs operations very quickly. Accordingly, we recommended the use of a "poll" interval.

    The following example implements the FindFirstWithTimeout method and calls FindFirst repeatedly (with a delay) until the specified UI element is located:

    public static class AutomationElementExtensions {
            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;
                }
            }
            

The following test performs the following:

  • Inputs wrong login and password.
  • Ensures that an error message is displayed within the "LogIn" form.
[Test]
public void NonExistingUsernameLoginTest() {
    afterLogInAction = CheckErrorLabel;
    LogIn("TestNonExistingUser", "123456");
}
void CheckErrorLabel() {
    AutomationElement errorLabelElement = loginForm.FindFirstByNameWithTimeout(
    	TreeScope.Children,
        "Invalid User or Password",
        10000);
    Assert.IsNotNull(errorLabelElement);
}
void LogIn(string username, string password) {
    // Finds the LogIn form and its main UI elements.
    loginForm = AutomationElement.RootElement.FindFirstByNameWithTimeout(
    											TreeScope.Children,
                                                logInFormAccessbleName,
                                                10000);
    AutomationElement usernameElement = loginForm.FindFirstByNameWithTimeout(TreeScope.Children, usernameAccessbleName, 10000);
    AutomationElement passwordElement = loginForm.FindFirstByNameWithTimeout(TreeScope.Children, passwordAccessbleName, 10000);
    AutomationElement logInButtonElement = loginForm.FindFirstByNameWithTimeout(TreeScope.Children, logInButtonAccessbleName, 10000);

    // Gets automation patterns to fill "UserName" and "Password" inputs (editors).
    ValuePattern usernameValuePattern = (ValuePattern)usernameElement.GetCurrentPattern(ValuePattern.Pattern);
    ValuePattern passwordValuePattern = (ValuePattern)passwordElement.GetCurrentPattern(ValuePattern.Pattern);
    InvokePattern invokePattern = (InvokePattern)logInButtonElement.GetCurrentPattern(InvokePattern.Pattern);

    // Sets editor values. Fills in username and password input fields.
    usernameValuePattern.SetValue(username);
    passwordValuePattern.SetValue(password);
    invokePattern.Invoke();

    // Performs an action after a log in attempt.
    afterLogInAction?.Invoke();
}

As you can see, writing a test comes down to getting an AutomationElement and calling its pattern methods.

4. Create a Test for the Customers Form

Let's consider a more complex case and test data editing in the DevExpress WinForms Data Grid (GridControl). The DevExpress Data Grid includes an "Is Modified" unbound column with Boolean values. Values in this column indicate whether the user has modified values in the "Name" column.

I'm using TablePattern to work with the Grid Control. The following example shows how to write a test that modifies customer name in our WinForms Grid and checks whether the value in the "Is Modified" column has changed from false to true:

[Test]
public void ModifiedCustomerTest() {
    LogIn(testExistingUserLogin, testExistingUserPassword);

    // Finds the GridControl and gets its TablePattern.
    customersForm = AutomationElement.RootElement.FindFirstByNameWithTimeout(
    													TreeScope.Children,
                                                        customersFormAccessbleName,
                                                        10000);
    AutomationElement customersGrid = customersForm.FindFirstByIdWithTimeout(
    													TreeScope.Children,
                                                        customersGridAutomationID,
                                                        10000);
    TablePattern customersTablePattern = (TablePattern)customersGrid.GetCurrentPattern(TablePattern.Pattern);

    // Activates a cell within the GridControl.
    AutomationElement cellToUpdate = customersTablePattern.GetItem(1, 1);
    InvokePattern testCellInvokePattern = (InvokePattern)cellToUpdate.GetCurrentPattern(InvokePattern.Pattern);
    testCellInvokePattern.Invoke();

    // Modifies the cell's value.
    AutomationElement editingControl = customersGrid.FindFirstByNameWithTimeout(TreeScope.Descendants, "Editing control", 1000);
    ValuePattern editedCellValuePattern = (ValuePattern)editingControl.GetCurrentPattern(ValuePattern.Pattern);
    editedCellValuePattern.SetValue("Value updated!");
    Thread.Sleep(1000); // Sets a delay for demonstration purposes.

    // Selects the next data row.
    AutomationElement nextRowCell = customersTablePattern.GetItem(2, 1);
    SelectionItemPattern selectionItemPattern = (SelectionItemPattern)TreeWalker.ControlViewWalker.GetParent(nextRowCell).GetCurrentPattern(SelectionItemPattern.Pattern);
    selectionItemPattern.Select();
    Thread.Sleep(1000); 

    // Checks if the value in the "Is Modified" column has changed.
    int isModiedColumnIndex = customersTablePattern.Current.GetColumnHeaders().ToList().FindIndex(h => h.Current.Name == "Is Modified");
    AutomationElement isModifiedCell = customersTablePattern.GetItem(1, isModiedColumnIndex);
    ValuePattern isModifiedCellValuePattern = (ValuePattern)isModifiedCell.GetCurrentPattern(ValuePattern.Pattern);
    Assert.AreEqual(isModifiedCellValuePattern.Current.Value, "Checked");
}

5. Run Tests

To run the tests I've just created, I'll expand the project with tests ("TestRunner"), right-click the *.cs file to invoke the context menu and click "Run Tests".

Download Demo Application

If UI testing is important to you and you'd like to download the WinForms application (with NUnit tests) used in this blog post, please visit:

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


Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.