使用 Microsoft Automation Framework 自动化 UI






4.93/5 (34投票s)
了解如何利用 UI 自动化来测试 UI 并支持可访问性功能。
引言
UI 自动化是应用程序用户界面的程序化接口,它允许外部应用程序与您的 UI 进行程序化通信。实际上,UI 自动化是 Windows 中辅助功能的主要促进者,它允许屏幕阅读器等不了解您的应用程序的外部应用程序也能轻松地与您的应用程序进行交互。
我在 2005 年任职于微软,当时我在 Visual Studio Team Architects 团队的一个名为 Whitehorse 的项目上担任软件开发测试人员,那时我开始接触 UI 自动化。对于需要一些背景信息的人来说,Whitehorse 在 Visual Studio 中包含 SOA 图,(您可以在 MSDN 杂志 这里了解更多关于该项目的信息)。产品测试团队对可视化设计器的测试完全使用了一个基于 Microsoft Active Accessibility 框架的内部 UI 测试自动化框架。
UI 自动化框架对于 Windows 平台来说相对较新,是 Windows Active Accessibility 的继承者。该框架提供了一种统一的方式来发现使用以下任何桌面技术构建的应用程序中的 UI 控件:Win32、Windows Forms 或 WPF。
在应用程序中使用 UI 自动化有几个好处
- 辅助功能 - UI 自动化为您的应用程序提供辅助功能支持。当您的应用程序所在的领域涉及法律合规性时(例如,政府法规),辅助功能支持变得更加重要。
- 自动化 UI 测试 - UI 自动化可以自动化应用程序 UI 的测试,从而节省手动测试和回归测试相关的时间和成本。此外,给定一组用例,UI 自动化可用于通过场景测试来验证应用程序行为。
- 自定义控件 - 如果您正在创作自定义控件,UI 自动化支持可以使您的控件的最终客户在他们的应用程序 UI 中自动化您的控件。
自动化如何工作?
UI 自动化提供了一种通用的协议,用于在您的应用程序和外部实体(如屏幕阅读器或自动化 UI 测试)之间交换信息。UI 自动化提供了一个 API,外部应用程序可以使用该 API 来发现控件、导航可视化树、访问控件的状态以及执行控件特定的操作。
在 WPF 中,自动化对象模型通过与 UIElement 关联的 System.Windows.Automation.AutomationElement 实例提供(称为控件的“自动化对等项”)。如果您正在创作自定义控件,您可能希望实现 System.Windows.Automation.Provider 命名空间下 UIAutomationProvider.dll 中定义的一个或多个接口来支持 UI 自动化。
为了支持 UI 自动化,控件作者需要实现 UIElement 类的一个 abstract 类 AutomationPeer,该类继承自 UIElement 类中名为 OnCreateAutomationPeer 的虚方法。AutomationPeer 然后在运行时用于提取 UIElement 的 AutomationElement。需要注意的是,WPF 中的标准控件具有与控件开箱即用关联的标准 AutomationPeer 实现。在创作 AutomationPeer 的派生类时,您可能希望继承一个标准实现,而不是直接从 AutomationPeer 派生并从头开始创作一个类。
下图显示了 AutomationPeer 的开箱即用派生类
 
 
每个 AutomationPeer 都必须实现一个或多个“标准控件模式”,以公开可供自动化客户端(如屏幕阅读器)使用的功能。控件可以公开由 PatternIntern 枚举的成员定义的零个或多个控件模式。

可以通过 AutomationPeer 的 GetPattern 方法查询控件是否支持特定的 PatternInterface,该方法接受 PatternInterface 成员作为参数并返回支持的模式 - BasePattern 的派生类或 null(如果指定的 PatternInterface 值与支持的模式不对应)。

BasePattern 有几个开箱即用的派生类,它们针对标准控件模式。

例如,Button、Hyperlink 和 MenuItem 控件支持 InvokePattern,因此可以向自动化客户端指示控件能够调用命令。类似地,扩展器控件支持 ExpandCollapsePattern 以指示控件能够展开或折叠其内部的内容。
控件模式是严格限制的,不支持自定义控件模式。原因是自动化客户端必须使用标准化协议,并且无法解释自定义模式来弄清楚控件的功能。
由于 UI 自动化在 WPF 中是可选的,您可以通过 AutomationProperties 静态类中指定的附加属性为控件附加自动化属性。

例如,如果我们想为一个文本框指定自动化 ID,我们可以在 XAML 中这样指定:
<TextBox Text=”{Binding Telephone}” AutomationProperties.Id=”ID_TXT_TELEPHONE”/> 
在 WPF 中导航自动化树
与可视化树导航类似,您可以使用 AutomationElement 实例在 WPF 中导航 UI 自动化树。但是,有一个重要的区别 - 与可视化树不同,自动化树不是预先构建的,而是在自动化树导航期间才构建的。这是因为与可视化树不同,UI 自动化在应用程序中是可选的。根据您想要的自动化树视图,有多种导航自动化树的方法。
- 原始视图 - 原始视图指的是未经筛选的完整自动化树视图。可以使用 TreeWalker类的RawViewWalker属性来导航原始视图。
- 控件视图 - 控件视图是原始视图的一个子集,它只包含与控件对应的 AutomationElement(换句话说,它们的AutomationElement.IsControlElementProperty属性已设置)。可以使用TreeWalker类的ControlViewWalker属性来导航控件视图。
- 内容视图 - 内容视图是控件视图的一个子集,它只包含表示用户可感知信息元素的 AutomationElement(换句话说,它们的AutomationElement.IsContentElementProperty属性已设置)。可以使用TreeWalker类的ContentViewWalker属性来导航内容视图。
- 自定义视图 - 使用 AutomationElement和一个条件(可以是PropertyCondition、AndCondition、OrCondition或NotCondition之一),您可以以自定义方式导航自动化树。要导航自定义视图,需要调用AutomationElement的FirstFind或FindAll方法,并提供树范围(TreeScope枚举,用于指定Element、Children、Descendants、Subtree、Parent和Ancestors或这些标志的任何组合的级别)和一个条件。
自动化树的根由静态属性 AutomationElement.RootElement 表示,它引用用户的桌面。您可以通过 RootElement 导航自动化树,或者通常更有效地从应用程序窗口对应的 AutomationElement 开始,该 AutomationElement 可以通过 AutomationElement.FromHandle 方法(通过传递应用程序窗口的句柄)获得。
实际示例 - 自动化 Windows 计算器
作为实际示例,让我们编写使用 UI 自动化测试 Windows 计算器的测试用例。测试的目的是测试计算器的以下方面:
- 数据输入验证 - 验证键入的数字是否正确显示在计算器中
- 编辑选项 - 验证复制和粘贴在计算器中是否按预期工作
- 计算验证 - 验证给定表达式树,计算器是否正确计算表达式。
计算器类
我们将从建模 Calculator 类开始,该类将是我们的 Windows 计算器接口。计算器类将在构造时启动 Windows 计算器的一个实例,并提供操作计算器的方法。此外,该类还将实现 IDisposable 接口,并在测试结束后释放计算器进程。每个测试使用干净的计算器实例可确保 no d。
以下是 Calculator 类的构造函数
public Calculator() 
{ 
    _calculatorProcess = Process.Start("Calc.exe");
    int ct = 0; 
    do 
    { 
        _calculatorAutomationElement = AutomationElement.RootElement.FindFirst
	(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, 
	"Calculator")); 
        ++ct; 
        Thread.Sleep(100); 
    } 
    while (_calculatorAutomationElement == null && ct < 50);
    if (_calculatorAutomationElement == null) 
    { 
        throw new InvalidOperationException("Calculator must be running"); 
    }
    _resultTextBoxAutomationElement = _calculatorAutomationElement.FindFirst
	(TreeScope.Descendants, new PropertyCondition
	(AutomationElement.AutomationIdProperty, "150"));
    if (_resultTextBoxAutomationElement == null) 
    { 
        throw new InvalidOperationException("Could not find result box"); 
    }
    GetInvokePattern(GetFunctionButton(Functions.Clear)).Invoke(); 
}
代码启动计算器进程并等待自动化元素可用。然后,它继续发现并初始化将用于与计算器交互的自动化元素。例如,要访问计算器结果文本框,我们在主计算器自动化元素内查找 ID 为 150 的自动化 ID。元素的自动化 ID 是使用 Visual Studio 附带的 Spy++ 工具通过以下步骤获得的:
- 启动一个计算器实例,然后启动 Spy++。
- 在计算器中键入任何数字(我键入了 1234)以在 Spy++ 中标识它。
- 在 Spy++ 中按 Ctrl + F3,然后从搜索窗口将十字准星拖到计算器实例。这将在 Spy++ 中突出显示计算器进程。
- 查找与按下的数字对应的窗口实例并单击以选择它。
- 右键单击选定的窗口,然后选择“属性”菜单项以打开属性窗口。
- 在“属性检查器”窗口中查找控件 ID。这是控件的自动化 ID(十六进制)。在使用到代码中之前,将其转换为十进制。在我这里,十六进制值 96 对应于十进制的 150。这就是我得到 150 的方式!
 
 

Dispose 方法仅终止进程。
public void Dispose() 
{ 
    _calculatorProcess.CloseMainWindow(); 
    _calculatorProcess.Dispose(); 
}
要获取 InvokePattern 来调用按钮,我们使用一个名为 GetInvokePattern 的实用方法,用于指定的 AutomationElement。
public InvokePattern GetInvokePattern(AutomationElement element) 
{ 
    return element.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern; 
}
函数按钮的 AutomationElement 可以通过 GetFunctionButton 方法检索,该方法以函数(例如,“Clear”)的 string 作为输入。
public AutomationElement GetFunctionButton(string functionName) 
{     
    AutomationElement functionButton = _calculatorAutomationElement.FindFirst
	(TreeScope.Descendants, new PropertyCondition
	(AutomationElement.NameProperty, functionName));
    if (functionButton == null) 
    { 
        throw new InvalidOperationException("No function button found with name: " + 
		functionName); 
    }
    return functionButton; 
}
所有函数名称都定义在 Functions 静态类中。
public class Functions 
{ 
    // Functions 
    public const string MemoryClear = "Memory clear"; 
    public const string Backspace = "Backspace"; 
    public const string MemoryRecall = "Memory recall";     
    public const string ClearEntry = "Clear entry"; 
    public const string MemoryStore = "Memory store"; 
    public const string Clear = "Clear"; 
    public const string DecimalSeparator = "Decimal separator";     
    public const string MemoryAdd = "Memory add"; 
    public const string MemoryRemove = "Memory subtract"; 
    public const string Equals = "Equals"; 
} 
类似地,我们使用 GetDigitButton 方法查询数字按钮,该方法以数字作为输入并返回关联的 AutomationElement。
public AutomationElement GetDigitButton(int number) 
{ 
    if ((number < 0) || (number > 9)) 
    { 
        throw new InvalidOperationException("number must be a digit 0-9"); 
    }
    AutomationElement buttonElement = _calculatorAutomationElement.FindFirst
	(TreeScope.Descendants, new PropertyCondition
	(AutomationElement.NameProperty, number.ToString()));
    if (buttonElement == null) 
    { 
        throw new InvalidOperationException
	("Could not find button corresponding to digit" + number); 
    }
    return buttonElement; 
}
需要注意的一点是使用了 AutomationElement.NameProperty 来查询元素。Spy++ 无法看到此属性,我不得不在调试器中打开并检查该对象才能找到它(我使用 AutomationId 加载了一个元素并在调试器中对其进行查询以找到名称属性)。
可以通过 Result 属性检索计算器的结果。此属性的 setter 从左到右解析字符 string,定位计算器上对应于该字符的按钮的 AutomationElement,然后使用 InvokePattern 的 Invoke 方法调用它。这实际上是在模拟用户在计算器中键入数字。
public object Result 
{ 
    get 
    { 
        return _resultTextBoxAutomationElement.GetCurrentPropertyValue
			(AutomationElement.NameProperty); 
    } 
    set 
    { 
        string stringRep = value.ToString();
        for (int index = 0; index < stringRep.Length; index++) 
        { 
            int leftDigit = int.Parse(stringRep[index].ToString());
            GetInvokePattern(GetDigitButton(leftDigit)).Invoke(); 
        } 
    } 
}
Evaluate、Clear 和 InvokeFunction 方法分别简单地评估(模拟按下“=”按钮)、清除(模拟按下“C”按钮)和调用一个函数。
public void Evaluate() 
{ 
    InvokeFunction(Functions.Equals); 
}
public void Clear() 
{ 
    InvokeFunction(Functions.Clear); 
}
public void InvokeFunction(string functionName) 
{     
    GetInvokePattern(GetFunctionButton(functionName)).Invoke(); 
}
FindMenu 方法定位指定的计算器菜单并返回其 ExpandCollapsePattern。
private ExpandCollapsePattern FindMenu(CalculatorMenu menu)  
{    
    AutomationElement menuElement = _calculatorAutomationElement.FindFirst
	(TreeScope.Descendants, new PropertyCondition
	(AutomationElement.NameProperty, menu.ToString()));     
    ExpandCollapsePattern expPattern = menuElement.GetCurrentPattern
		(ExpandCollapsePattern.Pattern) as ExpandCollapsePattern;  
    return expPattern; 
} 
OpenMenu 和 CloseMenu 使用 FindMenu 获取菜单的 ExpandCollapsePattern,然后分别展开和折叠菜单。ExecuteByMenuName 在展开的菜单中查找菜单项,并使用该菜单的 InvokePattern 调用命令。
public void OpenMenu(CalculatorMenu menu) 
{ 
    ExpandCollapsePattern expPattern = FindMenu(menu); 
    expPattern.Expand(); 
}
public void CloseMenu(CalculatorMenu menu) 
{ 
    ExpandCollapsePattern expPattern = FindMenu(menu); 
    expPattern.Collapse(); 
}
public void ExecuteMenuByName(string menuName) 
{ 
    AutomationElement menuElement = _calculatorAutomationElement.FindFirst
	(TreeScope.Descendants, new PropertyCondition
	(AutomationElement.NameProperty, menuName)); 
    
    if (menuElement == null) 
    { 
        return; 
    }
    InvokePattern invokePattern = menuElement.GetCurrentPattern
			(InvokePattern.Pattern) as InvokePattern; 
    
    if (invokePattern != null) 
    { 
        invokePattern.Invoke();    
    } 
}
现在我们已经公开了我们要与之交互的计算器功能,我们可以继续建模表达式树,以便在计算器中评估表达式。为了测试表达式评估,我们需要两种表达式计算模式:通过计算器 UI 进行表达式评估(计算的表达式)和通过代码进行评估(预期的评估)。如果计算的表达式等于预期的表达式,我们可以断言计算器以正确的方式计算表达式。此评估选项通过 EvaluateOption 枚举捕获。
internal enum EvaluateOption 
{ 
    UIEvaluate, 
    ActualEvaluate 
}
在最底层,我们需要建模具有子表达式需要评估的操作数。操作数通过 IOperand 接口进行建模。
internal interface IOperand 
{ 
    int Evaluate(EvaluateOption option, Calculator calculator); 
}
在基本层面,运算符是一个操作数。
internal abstract class Operator : IOperand 
{        
    internal Operator(string automationName) 
    { 
        AutomationName = automationName; 
    }
    public string AutomationName { private set; get; }
    public abstract int Evaluate(EvaluateOption option, Calculator calculator);
    protected virtual void InvokeOperator(Calculator calculator) 
    { 
        calculator.InvokeFunction(AutomationName); 
    } 
}
自动化名称是指 Operators 类中的常量,这些常量对应于计算器中的函数名称(即 AutomationElement.NameProperty 值)。
internal class Operators 
{ 
    public const string Negate = "Negate"; 
    public const string Divide = "Divide"; 
    public const string Multiply = "Multiply"; 
    public const string Subtract = "Subtract"; 
    public const string Add = "Add"; 
    public const string Sqrt = "Square root"; 
    public const string Percentage = "Percentage"; 
    public const string Reciprocal = "Reciprocal"; 
}
二元运算符使用 BinaryOperator abstract 类表示。
internal abstract class BinaryOperator : Operator 
{ 
    public BinaryOperator(string automationName) 
        : base(automationName) 
    { 
    }
    public IOperand Left { get; set; } 
        
    public IOperand Right { get; set; }
    public override int Evaluate(EvaluateOption option, Calculator calculator) 
    { 
        VerifyEvaluationState(); 
            
        int result = 0;
        switch (option) 
        { 
            case EvaluateOption.UIEvaluate: 
                calculator.Clear(); 
                int leftValue = Left.Evaluate(option, calculator);
                calculator.Clear(); 
                int rightValue = Right.Evaluate(option, calculator);
                calculator.Clear();
                calculator.Result = leftValue;
                InvokeOperator(calculator);
                calculator.Result = rightValue;
                calculator.Evaluate();
                result = int.Parse(calculator.Result.ToString());
             break;
            case EvaluateOption.ActualEvaluate: 
                result = Evaluate(Left.Evaluate(option, calculator), 
				Right.Evaluate(option, calculator)); 
            break;        
        }
        return result; 
    }
    protected void VerifyEvaluationState() 
    { 
        if ((Left == null) || (Right == null)) 
        { 
            throw new InvalidOperationException(); 
        } 
    }
    protected abstract int Evaluate(int left, int right); 
}
可以看到,如果 Evaluate 函数中使用 EvaluateOption.UIEvaluate,则使用 UI 自动化来模拟用户在计算器中的输入;否则,表达式的评估在代码中完成。其他 Evaluate 重载实现在派生类中,如 AddOperator、SubtractOperator、MultiplyOperator 和 DivideOperator。AddOperator 类如下(其余运算符非常相似,仅在实际计算和构造函数中的自动化名称上有所不同)。
internal class AddOperator : BinaryOperator 
{        
    public AddOperator() 
        : base(Operators.Add) 
    { 
    }
    protected override int Evaluate(int left, int right) 
    { 
        return left + right; 
    } 
}
NumberOperator 只是表达式树中的一个数字常量。
internal class NumberOperator : Operator 
{ 
    public NumberOperator(int number) 
        : base (null) 
    { 
        _number = number; 
    }
    public override int Evaluate(EvaluateOption option, Calculator calculator) 
    { 
        return _number; 
    }
    private readonly int _number; 
}
ExpressionTree 类通过 CreateTree 方法从 XML 流创建表达式树。
internal static class ExpressionTree 
{ 
    internal static IOperand CreateTree(Stream stream) 
    { 
        XDocument doc = XDocument.Load(stream); 
        return CreateOperend(doc.Root); 
    }
    private static IOperand CreateOperend(XElement operandElement) 
    { 
        XAttribute type = operandElement.Attribute("Type");
        IOperand operand = null;
        switch (type.Value) 
        { 
            case "NumberOperator": 
                operand = new NumberOperator(int.Parse
		(operandElement.Attribute("Value").Value)); 
            break;
            default: 
                string qualifyingName = "CalculatorTests." + type.Value; 
                operand = Activator.CreateInstance
		(Type.GetType(qualifyingName)) as IOperand;
                List<XNode> childNodes = new List<XNode>(operandElement.Nodes());
                if (operand is BinaryOperator) 
                { 
                    BinaryOperator binaryOperator = operand as BinaryOperator; 
                    binaryOperator.Left = CreateOperend(childNodes[0] as XElement); 
                    binaryOperator.Right = CreateOperend(childNodes[1] as XElement); 
                } 
                else if (operand is UnaryOperator) 
                { 
                    UnaryOperator unaryOperator = operand as UnaryOperator; 
                    unaryOperator.Operand = CreateOperend(childNodes[0] as XElement); 
                } 
                break; 
        }
        return operand; 
    } 
}
CreateTree 调用 CreateOperend 方法,该方法解析 XElement 的 Type 属性以确定操作数的类型,并根据类型是 NumberOperator(在这种情况下,它查找 Value 属性)还是其他类型(一元或二元操作数,在这种情况下,它查找子 Operand XML 元素),它递归地创建并返回一个 IOperand(如果需要)。最后,返回根操作数。例如,表达式 (6 - 1) + (7 * 9) 用 XML 表示如下。
<?xml version="1.0" encoding="utf-8" ?> 
<Operand Type="AddOperator"> 
  <Operand Type="SubtractOperator"> 
    <Operand Type="NumberOperator" Value="6"/> 
    <Operand Type="NumberOperator" Value="1"/> 
  </Operand> 
  <Operand Type="MultiplyOperator"> 
    <Operand Type="NumberOperator" Value="7"/> 
    <Operand Type="NumberOperator" Value="9"/> 
  </Operand> 
</Operand> 
现在我们已经成功创建了与计算器交互和评估表达式树的基础设施,我们可以开始编写测试用例了。
- 验证计算器中随机数字的数据输入,并验证结果是否正确显示在 UI 中。[TestMethod] public void TypeRandomNumber() { using (Calculator calc = new Calculator()) { int number = new Random().Next(100, 10000); string stringRep = number.ToString(); calc.Result = stringRep; Assert.AreEqual(stringRep, calc.Result); } }
- 验证剪切和粘贴功能。[TestMethod] public void VerifyCopyPaste() { using (Calculator calc = new Calculator()) { string stringRep = new Random().Next(100, 10000).ToString(); calc.Result = stringRep; calc.OpenMenu(Calculator.CalculatorMenu.Edit); calc.ExecuteMenuByName("Copy"); calc.Clear(); calc.OpenMenu(Calculator.CalculatorMenu.Edit); calc.ExecuteMenuByName("Paste"); Assert.AreEqual(stringRep, calc.Result); } }
- 验证从嵌入的 XML 资源文件中计算表达式树。[TestMethod] public void VerifyExpressionTrees() { string[] files = new[] { "CalculatorTests.Resources.SimpleNumberOperator.xml", "CalculatorTests.Resources.SimpleAdditionOperator.xml", "CalculatorTests.Resources.MixedOperators.xml" }; using (Calculator calc = new Calculator()) { foreach (string file in files) { calc.Clear(); IOperand expression = LoadExpressionTreeFromFile(file); Assert.AreEqual(expression.Evaluate (EvaluateOption.ActualEvaluate, calc), expression.Evaluate(EvaluateOption.UIEvaluate, calc)); } } } private IOperand LoadExpressionTreeFromFile(string resourceFileName) { return ExpressionTree.CreateTree (this.GetType().Assembly.GetManifestResourceStream(resourceFileName)); }
您可以使用 Visual Studio 测试管理器运行这些测试,您将看到计算器实例弹出,控件根据测试用例进行操作,然后计算器进程终止(请记住 Calculator 类中的 IDisposable 实现,它负责清理)。
好了,您刚刚看到了一个自动化客户端的实际应用!请继续关注我的下一篇博文,届时我将演示如何在开发自定义控件时实现自动化支持。
您还可以查看基于 UI 自动化框架且旨在简化 UI 自动化编程模型的 CodePlex 上的 White 项目:CodePlex。
历史
- 2011 年 1 月 1 日:初始发布


