自動化UI測試(UI自動化、Appium、編碼UI)
用戶界面 (UI) 測試可驗證應用程序的所有視覺元素是否正常運行。UI測試可以由測試人員手動執(zhí)行,也可以借助自動化測試工具執(zhí)行,自動化測試更快、更可靠且更具成本效益。
微軟編碼UI測試(CUIT)框架
編碼UI測試框架是微軟的一個解決方案,它利用控件的可訪問性層來記錄和運行UI測試,CUIT組件通過Visual Studio Installer分發(fā)。
該解決方案在Visual Studio 2019及以后被宣布過時,在Visual Studio 2022中,您仍然可以運行已編碼UI測試,但不能記錄新測試,較新的IDE版本將完全放棄對CUIT的支持。
參見:
DevExpress編碼UI擴展
DevExpress Coded UI是Microsoft Coded UI Tests的擴展,專為基于DevExpress的應用程序量身定制。這些解決方案之間的區(qū)別在于與Microsoft CUIT不同,DevExpress編碼UI擴展不利用輔助功能,該框架通過專有通道與控件進行通信,并使用DevExpress控件中聲明的幫助程序類。
Microsoft 終止CUIT的決定也會影響DevExpress編碼UI擴展,對于較新的項目,我們建議您改用Appium或UI Automation。
也可以看看:
- 。
- 。
Appium和UI自動化
Appium是一款開源工具,可讓您為 Web、混合、iOS 移動、Android 移動和 Windows 桌面平臺創(chuàng)建自動化UI測試,要測試Windows應用程序則需要設置。
也可以看看:
- — Appium 文檔。
- — 帶有示例的 DevExpress 博客文章。
Appium(以及多個其他測試框架)利用UI Automation ——Microsoft 的Windows輔助功能框架,您可以直接使用此框架(不涉及任何第三方解決方案)來編寫UI測試。
也可以看看:
- — 來自 Microsoft 的概述文章。
Appium和UI Automation 之間的選擇取決于場景和測試要求的復雜性,Appium更容易使用,但也有更多限制,因為它沒有實現(xiàn)所有UIA功能。例如,Appium 允許您使用 成員,但只能使用屬性,不能使用方法。
提示:調度程序、富編輯器、PDF查看器和電子表格控件目前不支持UI自動化。
步驟記錄器和手動測試腳本
大多數(shù)測試自動化平臺都提供了記錄工具,這些工具在運行時跟蹤您的操作(光標移動、單擊和鍵盤按鍵),并生成模擬這些操作的代碼。下面的博客文章展示了如何使用Appium步進記錄器與DevExpress控件:。
記錄器允許您編寫更少的代碼,但它們可能產(chǎn)生不穩(wěn)定的測試并導致性能問題。例如,大多數(shù)測試記錄器在元素選擇代碼中枚舉目標UI元素的所有父元素,因此,一個小的UI修改(比如添加一個新的Panel容器)會導致這個選擇代碼失敗。
為了避免潛在的問題并更好地理解測試的功能,我們建議手動編寫測試腳本。例如,您可以選擇為目標UI元素檢查哪些父控件,而不是列出元素父元素的整個層次結構,或者直接獲取該元素而不訪問其任何父元素。
如何編寫Appium和UI自動化測試
常用測試結構
Appium和UI自動化測試共享類似的代碼塊層次結構,每個塊都由一個 NUnit屬性裝飾。
修飾包含測試的類。
每次測試即將開始時,都會調用帶有此屬性的方法。
與SetUp屬性相反,此屬性修飾每次測試完成時執(zhí)行的一組指令。
修飾一個包含測試腳本的方法。
Appium和UIA測試的一般實現(xiàn)如下所示:
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或名稱獲取該元素。
- 檢查它支持哪些模式,并利用這些模式的屬性和方法來模擬用戶操作。
- 調用 方法來比較實際和預期的控制狀態(tài)。
要獲取元素名稱和 ID,并檢查其可用的模式 API,請使用Microsoft Inspect —— Windows SDK安裝中包含的免費工具。
 
 
手工檢查UI元素還允許您定位不良的可訪問性名稱和其他問題,要解決這些問題,請?zhí)幚鞤XAccessible.QueryAccessibleInfo事件。
如何編寫 Appium 測試
- 在 Windows 設置中啟用。
- 下載、安裝并運行 。
- 在需要測試的項目中打開全局WindowsFormsSettings.UseUIAutomation。
- 在 Visual Studio 中創(chuàng)建一個新的“單元測試項目” 。
- 安裝“Appium.WebDriver” NuGet 包。
- 根據(jù)通用測試結構部分創(chuàng)建測試,下面的代碼說明了一個自動化測試示例。
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("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.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("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
- 上面的代碼借助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("http://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("http://127.0.0.1:4723"), globalDriverOptions)
Dim menuItem = globalDriver.FindElementByName("ItemName")
	點擊復制
如何編寫 UI 自動化測試
- 在需要測試的項目中打開全局WindowsFormsSettings.UseUIAutomation屬性。
- 在Visual Studio中創(chuàng)建一個新的“Unit Test Project”。
- 在您的項目中包括UIAutomationClient.dll和UIAutomationTypes.dll庫。
- 根據(jù)公共測試結構部分創(chuàng)建測試,下面的代碼演示了一個自動化測試示例。
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測試類似,根據(jù)從Inspect復制的名稱或id檢索元素,使用 來查找所需的元素。
- 自定義FindFirstWithTimeout方法通過添加超時閾值來擴展FindFirst,此值指定當元素不能立即可用時,腳本可以重試獲取該元素的時間。
- 該類Mouse公開了允許模擬鼠標操作的方法,安裝“Microsoft.TestApi” NuGet 包后,此類即可使用,也可以使用其他方式來模擬單擊和指針移動。
- 模式方法(TablePattern.GetColumnHeaders()、ValuePattern.SetValue()等)允許您快速找到所需的元素、設置新的控件值、執(zhí)行默認控件操作(例如單擊)等等,正如在Appium和UI自動化一節(jié)中提到的,這些方法在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("http://127.0.0.1:4723"), globalDriverOptions)
Dim menuItem = globalDriver.FindElementByName("ItemName")                
            
 QQ交談
QQ交談 在線咨詢
在線咨詢 
                 
                
 渝公網(wǎng)安備
            50010702500608號
渝公網(wǎng)安備
            50010702500608號
             
            
 客服熱線
客服熱線