自动化UI测试(UI自动化、Appium、编码UI)
用户界面 (UI) 测试可验证应用程序的所有视觉元素是否正常运行。UI测试可以由测试人员手动执行,也可以借助自动化测试工具执行,自动化测试更快、更可靠且更具成本效益。
微软编码UI测试(CUIT)框架
编码UI测试框架是微软的一个解决方案,它利用控件的可访问性层来记录和运行UI测试,CUIT组件通过Visual Studio Installer分发。
该解决方案在Visual Studio 2019及以后被宣布过时,在Visual Studio 2022中,您仍然可以运行已编码UI测试,但不能记录新测试,较新的IDE版本将完全放弃对CUIT的支持。
参见:
DevExpress编码UI扩展
DevExpress Coded UI是Microsoft Coded UI Tests的扩展,专为基于DevExpress的应用程序量身定制。这些解决方案之间的区别在于与Microsoft CUIT不同,DevExpress编码UI扩展不利用辅助功能,该框架通过专有通道与控件进行通信,并使用DevExpress控件中声明的帮助程序类。
Microsoft 终止CUIT的决定也会影响DevExpress编码UI扩展,对于较新的项目,我们建议您改用Appium或UI Automation。
也可以看看:
- 。
- 。
Appium和UI自动化
Appium是一款开源工具,可让您为 Web、混合、iOS 移动、Android 移动和 Windows 桌面平台创建自动化UI测试,要测试Windows应用程序则需要设置。
也可以看看:
- — Appium 文档。
- — 带有示例的 DevExpress 博客文章。
Appium(以及多个其他测试框架)利用UI Automation ——Microsoft 的Windows辅助功能框架,您可以直接使用此框架(不涉及任何第三方解决方案)来编写UI测试。
也可以看看:
- — 来自 Microsoft 的概述文章。
Appium和UI Automation 之间的选择取决于场景和测试要求的复杂性,Appium更容易使用,但也有更多限制,因为它没有实现所有UIA功能。例如,Appium 允许您使用 成员,但只能使用属性,不能使用方法。
提示:调度程序、富编辑器、PDF查看器和电子表格控件目前不支持UI自动化。
步骤记录器和手动测试脚本
大多数测试自动化平台都提供了记录工具,这些工具在运行时跟踪您的操作(光标移动、单击和键盘按键),并生成模拟这些操作的代码。下面的博客文章展示了如何使用Appium步进记录器与DevExpress控件:。
记录器允许您编写更少的代码,但它们可能产生不稳定的测试并导致性能问题。例如,大多数测试记录器在元素选择代码中枚举目标UI元素的所有父元素,因此,一个小的UI修改(比如添加一个新的Panel容器)会导致这个选择代码失败。
为了避免潜在的问题并更好地理解测试的功能,我们建议手动编写测试脚本。例如,您可以选择为目标UI元素检查哪些父控件,而不是列出元素父元素的整个层次结构,或者直接获取该元素而不访问其任何父元素。
如何编写Appium和UI自动化测试
常用测试结构
Appium和UI自动化测试共享类似的代码块层次结构,每个块都由一个 NUnit属性装饰。
修饰包含测试的类。
每次测试即将开始时,都会调用带有此属性的方法。
与SetUp属性相反,此属性修饰每次测试完成时执行的一组指令。
修饰一个包含测试脚本的方法。
Appium和UIA测试的一般实现如下所示:
C#:
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.NET:
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
检查Tool
要为任何UI元素编写测试,需要做以下事情:
- 通过ID或名称获取该元素。
- 检查它支持哪些模式,并利用这些模式的属性和方法来模拟用户操作。
- 调用 方法来比较实际和预期的控制状态。
要获取元素名称和 ID,并检查其可用的模式 API,请使用Microsoft Inspect —— Windows SDK安装中包含的免费工具。
手工检查UI元素还允许您定位不良的可访问性名称和其他问题,要解决这些问题,请处理DXAccessible.QueryAccessibleInfo事件。
如何编写 Appium 测试
- 在 Windows 设置中启用。
- 下载、安装并运行 。
- 在需要测试的项目中打开全局WindowsFormsSettings.UseUIAutomation。
- 在 Visual Studio 中创建一个新的“单元测试项目” 。
- 安装“Appium.WebDriver” NuGet 包。
- 根据通用测试结构部分创建测试,下面的代码说明了一个自动化测试示例。
C#:
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("//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.NET:
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("//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
- 上面的代码借助FindElementByName和FindElementByAccessibilityId方法定位所需的UI元素,要获取元素名称或ID,请在Inspect中浏览元素属性
- 要模拟鼠标单击和按键,请调用Click()和SendKeys方法。
- 使用UIElement.GetAttribute方法获取模式属性的值,这些名称在Inspect中也可见。
要访问模式的属性LegacyIAccessible,请使用“Legacy{PropertyName}”格式:
C#:
var value = progressBarAccordionItem.GetAttribute("LegacyState");
点击复制
VB.NET:
Dim value = progressBarAccordionItem.GetAttribute("LegacyState")
点击复制
其他模式的属性用“{PatternName}.{PropertyName}”格式访问:
C#:
var value = progressBar.GetAttribute("RangeValue.Maximum");
点击复制
VB.NET:
Dim value = progressBar.GetAttribute("RangeValue.Maximum")
点击复制
- DevExpress 上下文菜单没有直接所有者,因此它们的可访问对象是桌面窗口的子窗口,而不是应用程序窗口,要访问这些菜单中的项目,请使用桌面窗口驱动程序。
C#:
AppiumOptions globalDriverOptions = new AppiumOptions(); globalDriverOptions.AddAdditionalCapability("app", "Root"); var globalDriver = new WindowsDriver<WindowsElement>(new Uri("//127.0.0.1:4723"), globalDriverOptions); var menuItem = globalDriver.FindElementByName("ItemName");
点击复制
VB.NET:
Dim globalDriverOptions As AppiumOptions = New AppiumOptions() globalDriverOptions.AddAdditionalCapability("app", "Root") Dim globalDriver = New WindowsDriver(Of WindowsElement)(New Uri("//127.0.0.1:4723"), globalDriverOptions) Dim menuItem = globalDriver.FindElementByName("ItemName")
点击复制
如何编写 UI 自动化测试
- 在需要测试的项目中打开全局WindowsFormsSettings.UseUIAutomation属性。
- 在Visual Studio中创建一个新的“Unit Test Project”。
- 在您的项目中包括UIAutomationClient.dll和UIAutomationTypes.dll库。
- 根据公共测试结构部分创建测试,下面的代码演示了一个自动化测试示例。
C#:
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.NET:
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 <System.Runtime.CompilerServices.Extension> _ 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 <System.Runtime.CompilerServices.Extension> _ 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
- 与Appium测试类似,根据从Inspect复制的名称或id检索元素,使用 来查找所需的元素。
- 自定义FindFirstWithTimeout方法通过添加超时阈值来扩展FindFirst,此值指定当元素不能立即可用时,脚本可以重试获取该元素的时间。
- 该类Mouse公开了允许模拟鼠标操作的方法,安装“Microsoft.TestApi” NuGet 包后,此类即可使用,也可以使用其他方式来模拟单击和指针移动。
- 模式方法(TablePattern.GetColumnHeaders()、ValuePattern.SetValue()等)允许您快速找到所需的元素、设置新的控件值、执行默认控件操作(例如单击)等等,正如在Appium和UI自动化一节中提到的,这些方法在Appium中不可用。
- 要获得上下文菜单项,可以使用RootElements和TreeScope.Descendants。
C#:
AutomationElement menuItem = AutomationElement.RootElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "itemName")); ((InvokePattern)menuItem.GetCurrentPattern(InvokePattern.Pattern)).Invoke();
VB.NET:
Dim globalDriverOptions As AppiumOptions = New AppiumOptions() globalDriverOptions.AddAdditionalCapability("app", "Root") Dim globalDriver = New WindowsDriver(Of WindowsElement)(New Uri("//127.0.0.1:4723"), globalDriverOptions) Dim menuItem = globalDriver.FindElementByName("ItemName")