如何在WPF中对UI进行自动化测试(通过外部代码操作WPF内部UI元素)

记得早些年在深圳做设备的时候,公司测试使用了一款能自动测试WPF界面的工具,它能模拟点击,能记录步骤,也能获取输出。但具体名称忘记了。

因为WPF界面元素是没有句柄的,所以无法对其它进行外部控制(非软件内部),所以早些年只能对Win32原生/MFC/Delphi/Winform的一些界面通过WinApi进行外部控制。

也是最近在看代码时,看到了UIAutomation这个dll,就找资料来学习了一下。

 

UI自动化技术

Microsoft UI 自动化是适用于 Microsoft Windows 的新辅助功能框架,可在支持 Windows Presentation Foundation (WPF)的所有操作系统上使用。

UI 自动化提供对桌面上大多数用户界面 (UI) 元素的编程访问,使辅助技术产品(例如屏幕阅读器)能够使用标准输入以外的其他方式向最终用户提供有关 UI 的信息并操控 UI。 UI 自动化还使自动测试脚本能够与 UI 交互。

 

UI 自动化客户端应用程序可在保证其将在多个框架上运行的情况下进行编写。 UI 自动化核心掩盖了位于 UI 各组成部分之下的框架中的任何差异。 例如,在 UI 自动化视图中,WPF 按钮的 Content 属性、Win32 按钮的 Caption 属性和 HTML 图像的 ALT 属性全都映射到单个属性 Name

 

UI自动化提供的组件 

组件说明
提供程序 API(UIAutomationProvider.dllUIAutomationTypes.dll 一组由 UI 自动化提供程序实现的接口定义,以及一组提供有关 UI 元素的信息并响应编程输入的对象。
客户端 API(UIAutomationClient.dllUIAutomationTypes.dll 托管代码的一组类型,它们使 UI 自动化客户端应用程序可以获取有关 UI 的信息,并将输入发送到控件。
UiAutomationCore.dll 用于处理提供程序与客户端之间的通信的底层代码(有时称为 UI 自动化核心)。
UIAutomationClientsideProviders.dll 一组用于标准旧版本控件的 UI 自动化提供程序。 (WPF 控件原生支持 UI 自动化。)此支持自动提供给客户端应用程序。

 

本文主要介绍的是创建使用 UI 自动化核心来与UI 元素通信的应用程序(使用客户端 API)。

也就是通过外部代码来操作WPF UI元素

 

说明:这些组件都是.NET Framework自带的,支持对.NET Framework.NET Core开发的WPF应用程序进行UI自动化

 

准备工作

我们还需要了解UIAutomation里常用的一些类和方法。

 

AutomationElement 类

表示 UI 自动化树中的一个 UI 自动化元素,并包含由 UI 自动化客户端应用程序用作标识符的值。

 

常用属性

RootElement

获取当前桌面的根 AutomationElement

常用方法

方法名称 功能
FindAll(TreeScope, Condition)

返回满足指定条件的全部 AutomationElement 对象。

FindFirst(TreeScope, Condition)

返回与指定条件匹配的第一个子级或子代元素。

FromPoint(Point)

在桌面上指定点检索用户界面 (UI) 项的新 AutomationElement 对象。

SetFocus()

将焦点设置在 AutomationElement 上。

 

TreeScope枚举

Ancestors 16

指定搜索包括元素的上级(包括父级)。 不支持。

Children 2

指定搜索包括元素的直接子级。

Descendants 4

指定搜索包括元素的子代(包括子级)。

Element 1

指定搜索包括元素本身。

Parent 8

指定搜索包括元素的父级。 不支持。

Subtree 7

指定搜索包括搜索的根和全部子代。

 

PropertyCondition类

表示一个 Condition,它测试属性是否具有指定的值。

 

PropertyConditon常用的构造方法如下

1 public PropertyCondition (System.Windows.Automation.AutomationProperty property, object value);

 

property
类型:AutomationProperty
说明:要测试的属性。

value
类型:Object
说明:要测试属性的值。

 

AutomationProperty一般会取如下值:

AutomationElement.AutomationIdProperty

标识 AutomationId 属性,该属性用于标识元素。

AutomationElement.NameProperty

标识 Name 属性。

 

 

BasePattern 类

为控件模式类提供基实现。

 

它派生了以下类型

 

这里的模式指的是控件所具备 的功能,例如Button控件具备 InvokePattern,可以对其进行 点击 。TextBox控件具备 ValuePattern,可以对其进行赋值。

不同的控件所具备 的Pattern是不一样的,这个在后面会进行介绍 。

 

界面元素层级关系

编写测试自动化使用 Microsoft 的 UI 自动化库时, 必须了解如何识别待测试应用程序上的控件。在前面的文章中,我介绍过Snoop工具的使用(https://www.cnblogs.com/zhaotianff/p/18233083),

但当时只介绍了可视化树,并没有介绍UIAutomation树,这里进行补充一下。

当使用Snoop spy一个wpf程序后,在左上角的下拉可以切换到Automation树显示

 

然后在Automation树中选取元素或者通过Ctrl+Shift+鼠标选取,

例如这里我们选取Calc按钮,在右侧 的属性面板中,可以看到关于UIAutomation的相关属性

这里我们重点注意三个属性:

Name:控件显示的文本

AutomationId:控件名(在XAML里通过Name或x:Name指定)

SupportedPatterns:操作控件所支持的模式

通过Name属性和AutomationId属性可以对元素进行查找 ,通过SupportedPatterns属性,可以对元素进行操作,如点击、展开等。

 

 

快速实践

1、首先我们创建一个WPF应用程序,程序名称为UIAutomationDemo,界面代码如下

 1   <Grid>
 2       <Grid.RowDefinitions>
 3           <RowDefinition Height="auto"/>
 4           <RowDefinition/>
 5           <RowDefinition/>
 6           <RowDefinition/>
 7       </Grid.RowDefinitions>
 8 
 9       <Menu>
10           <MenuItem Header="File">
11               <MenuItem Header="Exit" Click="MenuItem_Click"></MenuItem>
12           </MenuItem>
13       </Menu>
14 
15       <StackPanel Orientation="Horizontal" Grid.Row="1">
16           <Label Content="num1" VerticalAlignment="Center" Margin="10"></Label>
17           <TextBox Width="150" VerticalAlignment="Center" Name="tbox1"></TextBox>
18 
19           <Label Content="num2" VerticalAlignment="Center" Margin="10"></Label>
20           <TextBox Width="150" VerticalAlignment="Center" Name="tbox2"></TextBox>
21       </StackPanel>
22 
23       <GroupBox Grid.Row="2">
24           <StackPanel  Orientation="Horizontal">
25               <RadioButton Width="88" Height="28" Content="+" VerticalAlignment="Top" x:Name="radio_Sum" Margin="10,5" GroupName="mode" IsChecked="True" Click="btn_Sum_Click"></RadioButton>
26               <RadioButton Width="88" Height="28" Content="-" VerticalAlignment="Top" x:Name="radio_Dec" Margin="10,5" GroupName="mode" Click="btn_Dec_Click"></RadioButton>
27               <RadioButton Width="88" Height="28" Content="*" VerticalAlignment="Top" x:Name="radio_Multi" Margin="10,5" GroupName="mode" Click="btn_Multi_Click"></RadioButton>
28           </StackPanel>
29       </GroupBox>
30 
31       <StackPanel Grid.Row="3" Orientation="Horizontal">
32           <Label Content="result" VerticalAlignment="Center" Margin="10"></Label>
33           <TextBox VerticalAlignment="Center" Width="200" Name="tbox_Result"></TextBox>
34 
35           <Button Margin="10" Content="Calc" VerticalAlignment="Center" Width="120" Name="btn_Calc" Click="btn_Calc_Click"></Button>
36       </StackPanel>
37   </Grid>

 

这是一个简单的计算器,运行效果如下:

 

 

接下来我们使用UI自动化,去模拟操作这个WPF程序的界面元素。

 

2、判断需要去执行UI自动化的程序是否运行

 1 //.Net Framework版本
 2 var nfxPath = "..\\..\\..\\UIAutomationDemo\\bin\\Debug\\UIAutomationDemo.exe";
 3 //.Net Core版本
 4 var corePath = "..\\..\\..\\UIAutomationDemoCore\\bin\\Debug\\net8.0-windows\\UIAutomationDemoCore.exe";
 5 
 6 //判断要UI自动化的程序是否已经运行
 7 var p = System.Diagnostics.Process.GetProcesses().FirstOrDefault(x => x.ProcessName == "UIAutomationDemo");
 8 
 9 if(p == null)
10 {
11     p = Process.Start("..\\..\\..\\UIAutomationDemo\\bin\\Debug\\UIAutomationDemo.exe");
12     p.WaitForInputIdle();
13 }
14 
15 if (p == null) 
16     throw new Exception("Failed to find UIAutomationDemo process"); 
17 else
18     Console.WriteLine("Found UIAutomationDemo process"); 

 

3、获取桌面元素

 桌面是顶级元素,所以需要先获取桌面

1             Console.WriteLine("Getting Desktop"); 
2             AutomationElement uiDesktop = null; 
3             uiDesktop = AutomationElement.RootElement; 
4             if (uiDesktop == null) 
5                 throw new Exception("Unable to get Desktop"); 
6             else 
7                 Console.WriteLine("Found Desktop\n");

 

4、获取主窗口

拿到桌面元素以后,我们就可以获取程序的主窗口了

通过Name属性(窗口标题)获取主窗口

1 AutomationElement uiMainWindow  = uiDesktop.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "MainWindow")); 

 

通过AutomationId属性(控件名称)获取主窗口

1 AutomationElement uiMainWindow = uiDesktop.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "UIAutomationDemo"));

 

 

5、获取界面元素

 

下面我们开始获取界面元素

1   //num1文本框
2   AutomationElement uiTextBox1 = uiMainWindow.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "tbox1"));
3   //num2文本框
4   AutomationElement uiTextBox2 = uiMainWindow.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "tbox2"));
5   //result文本框
6   var uiTextBoxResult = uiMainWindow.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "tbox_Result"));
7   //Calc按钮
8   AutomationElement calcButton =  uiMainWindow.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Calc")); 

 

Snoop中可以看到,TextBox的类型是Edit,所以我们也可以通过下面的方法获取所有的TextBox,其它控件也是类似的原理。

 

 

1 //获取所有TextBox元素
2 AutomationElementCollection uiAllTextBoxes = null;uiAllTextBoxes = uiMainWindow.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit));

 

但是这里在获取RadioButton时会遇到一个问题,获取出来的元素为空

1 AutomationElement radioSum = uiMainWindow.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "radio_Sum"));

 原因是因为这里我们用了一个GroupBox,又多了一层,如果使用TreeScope.Children到直接子级里面去查找,是找不到的。

可以使用TreeScope.Descedants来查询 

1 AutomationElement radioSum = uiMainWindow.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, "radio_Sum"));
2 AutomationElement radioDec = uiMainWindow.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, "radio_Dec"));
3 AutomationElement radioMulti = uiMainWindow.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, "radio_Multi"));

 

 也可以先获取上级的GroupBox,再获取RadioButton

1 //获取上级GroupBox 再获取 RadioButton
2 AutomationElement groupBoxElement = uiMainWindow.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty,ControlType.Group));
3 AutomationElement radio = groupBoxElement.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "radio_Sum"));

 

1  //获取菜单
2  AutomationElement uiFile = uiMainWindow.FindFirst(TreeScope.Descendants,new PropertyCondition(AutomationElement.NameProperty, "File"));

 

6、对界面元素进行操作

 

首先我们对两个文本框进行赋值

1  //num1赋值200 
2  ValuePattern vpTextBox1 = (ValuePattern)uiTextBox1.GetCurrentPattern(ValuePattern.Pattern);
3  vpTextBox1.SetValue("200");
4 
5  //num2赋值300
6  ValuePattern vpTextBox2 = (ValuePattern)uiTextBox2.GetCurrentPattern(ValuePattern.Pattern);
7  vpTextBox2.SetValue("300");

关于Pattern我们在前面进行过简单的介绍,对于不同控件所具备 的Pattern可以使用Snoop查看。不同Pattern的用法可以点击前面介绍Pattern那里的链接进行查看。

 

点击Calc按钮

1  InvokePattern ipCalcButton =(InvokePattern)calcButton.GetCurrentPattern(InvokePattern.Pattern);
2  ipCalcButton.Invoke();

 

获取结果

1 string result = (string)uiTextBoxResult.GetCurrentPropertyValue(ValuePattern.ValueProperty);

 

 切换模式

1  SelectionItemPattern spDecRadioButton =(SelectionItemPattern)radioDec.GetCurrentPattern(SelectionItemPattern.Pattern);
2  spDecRadioButton.Select();

 

展开【File】菜单

1  Thread.Sleep(5000);
2  ExpandCollapsePattern expClickFile = (ExpandCollapsePattern)uiFile.GetCurrentPattern(ExpandCollapsePattern.Pattern);
3  expClickFile.Expand();

 

子菜单元素一定要在菜单展开后才能获取,如果没有先展开是获取不到的

获取【Exit】菜单并点击

1 AutomationElement uiFileExit = uiMainWindow.FindFirst(TreeScope.Descendants,new PropertyCondition(AutomationElement.NameProperty, "Exit"));
2 InvokePattern ipFileExit =(InvokePattern)uiFileExit.GetCurrentPattern(InvokePattern.Pattern);
3 ipFileExit.Invoke();

 

完整运行效果

 

示例代码

下载 

 

参考资料

https://learn.microsoft.com/zh-cn/dotnet/framework/ui-automation/ui-automation-providers-for-managed-code

https://learn.microsoft.com/en-us/archive/msdn-magazine/2009/march/test-run-automating-ui-tests-in-wpf-applications配套代码

https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.automation.automationelement

posted @ 2025-03-18 15:26  zhaotianff  阅读(359)  评论(0)    收藏  举报