65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF UIAutomation 测试套件开发

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2019年9月24日

CPOL

15分钟阅读

viewsIcon

18514

downloadIcon

1408

WPF UI自动化测试指南。使测试代码易于编写和维护。

 

引言

软件的质量可以通过编码指南和自动化测试来确定。通常有单元测试来确保小单元(例如 `方法`)的质量符合预期。在足够大的项目中,还有其他自动化测试,例如用户界面(UI)测试,以确保用户体验符合预期。

本文讨论的是用户界面(UI)测试的自动化测试。我们使用微软最新的测试环境 CodedUI 作为示例,解释如何将自动化测试集成到软件开发迭代中。在撰写本文的过程中,我们发现 CodedUI 将随着 Visual Studio 2019 的结束而弃用,虽然距离本文 2019 年发布还有一段时间,但值得指出的是,本文讨论的许多原则也适用于其他 UI 测试框架。

HTML 的 UI 测试自动化已经存在一段时间了。虽然像 Selenium 这样的开源测试环境一直是行业标准,但也有像 Appium 这样的开源项目可用于测试 WPF 应用程序。

本文列出了在开发任何自动化 UI 测试时应实施的重要实践。我们解释了 CodeUI 测试框架的基本功能和用例,并利用它来展示如何扩展方法和属性,从而使测试目标中以前无法测试的部分可以通过自动化来测试。

使用微软 CodedUI 的一个重要前提是您已安装 Visual Studio Enterprise

目标

UI 测试自动化与普通软件开发有很多相似之处,但它也有独特的属性,使其区别于其他类型的开发。我们使用本节讨论在使用真实项目中的 UI 自动化时应遵守的最佳实践。为确保正常开发可以进行,尽管增加了 UI 测试自动化的工作量,仍应遵守以下高级目标

  • 在 WPF 应用的早期迭代中引入自动化测试,
  • 保持测试代码易于编写且简单易维护,
  • 保持测试套件易于部署和运行。

以上几点可以扩展为自动化测试的重要原则。这些原则确保您最终不仅仅测试任何东西,而是从项目最重要的点开始,然后逐渐处理不太重要的事情。这些原则是:

  • 外部接口的测试开始,
  • 保持每个测试方法的测试目标简单清晰(在一个测试方法中混合太多逻辑不是个好主意),
  • 不要修改测试对象或测试环境,
  • 尽量减少重复测试
    • 不可能测试所有组合,
    • 选择最有用的组合,
    • 避免冗余的测试覆盖,
  • 平衡开发进度和测试进度。

一些观点,例如上面的最后一点,解决了诸如可维护性和自动化 UI 测试额外工作量等重要问题。很容易看出,一个简单的项目和一个简单的用户界面几乎不值得为测试自动化付出额外的努力。但也有一些产品项目,由数百或数千名客户使用,由一个大型(去中心化)软件开发团队生产。这些项目可能会从测试自动化中受益,尽管付出了额外的努力,因为通过自动化进行的定期测试可以确保 UI 的所有部分在进入开发周期的最后阶段(修复错误)之前都能通过基本测试。

测试场景

在软件开发过程中可以执行许多类型的测试。它们是:

  • 单元测试,
  • 集成测试,
  • 功能测试,
  • 验收测试,
  • 性能测试,
  • 以及更多[1]

我们在下面的章节中详细介绍功能测试及其自动化,以说明如何通过代码测试用户界面(UI)。功能测试的开发与软件开发本身相似,因为我们可以应用最佳实践、模式和经验教训。一个“明显”的最佳实践是应用高级设计原则,该原则至少涉及两个原则,并且适用于每个测试:

  1. 从软件需求开始。识别测试需求至关重要,因为它将直接影响测试覆盖率。
     
  2. 对于每个测试需求,请始终使用
    1. 等价类划分 [2]
    2. 边界值分析 [3],并且
    3. 推断错误.
    以全面地制定测试用例。

一个全面的测试用例是从用户需求开始的,因为它确保用户可能的行为反映在所需的质量中。这可以通过使用列出的模式并花费时间推断到目前为止尚未评估的错误来确保。

功能测试(黑盒)

功能测试是一种软件测试,通过这种测试,软件会根据规范中的功能需求进行测试。功能测试也称为黑盒测试,因为测试将被测试的系统视为一个无法打开查看内部的盒子。这种类型的测试侧重于对系统输入、系统输出和测试期间行为的期望,而不是查看系统的内部细节。基于故事的功能需求如下所示:

  1. 用户启动系统,
  2. 用户更改程序设置,
  3. 用户点击关闭(按钮),
  4. 系统保存所有更改的程序设置,并且
  5. 系统停止运行。

针对此需求的测试将是确定软件确实保存了所有相关数据并正确关闭。此测试可以由测试工程师手动执行,但自动化相比手动方法具有许多优势。本节演示如何使用 UI Automation 为 WPF 设计和执行自动化功能测试。

此过程通常涉及以下步骤:

步骤 描述
设计
  1. 识别先决条件
  2. 识别软件预期执行的功能,以及
  3. 识别结果。
生成测试方法 使用 Microsoft UIAutomation API 模拟用户操作,或
  1. 使用 Visual Studio Coded UI 工具模拟用户操作并录制。
  2. 从录制中生成测试代码,并手动修改以使其更具可扩展性。例如,更新方法参数以支持数据驱动测试。
编译和执行 执行测试方法。
检查测试结果 比较实际输出和预期输出。

Microsoft UI Automation 是 Microsoft Windows 的一项新的辅助功能框架。它通过提供对用户界面(UI)信息的编程访问,满足了辅助技术产品和自动化测试框架的需求[4]

Visual Studio Coded UI [5] 封装了 UI Automation API,并提供了一个工具(`CodedUITestBuilder.exe`)来录制用户操作并自动生成测试代码。请注意,Visual Studio Coded UI 需要 Visual Studio Enterprise 和 Coded UI 测试组件进行录制。

我们将在接下来的章节中详细介绍使用上述工具进行的自动化功能测试。

测试示例

测试的测试目标通常是技术单元:控件、库或整个软件产品。本节中的测试目标是开源控件 AvalonEdit(版本 5.0.3)(GitHub 上的源代码),我们设计了一个测试用例来验证其字体设置功能。

设计

我们的示例文本用例设计如下:

  1. 启动 AvalonEdit.Sample 应用程序,
  2. 将字体大小设置为指定有效值(例如:16),
  3. 验证文本字体大小是否与上一步设置的值相同。

值得注意的是,上述测试用例设计与功能测试部分中确定的工作流程非常相似。这种相似性支持了应该由驱动测试目标的软件组件来模拟用户的观点,并且它支持将功能测试用例直接实现为自动化测试,或者实现给定功能测试的一个足够接近的变体的常见做法。

录制测试并生成测试方法

驱动测试目标的测试组件在我们的例子中是CodedUI 测试框架。使用 CodedUI 进行测试录制需要单独的设置和项目类型[5] [6]

测试步骤的录制和测试方法的生成(使用 _CodedUITestBuilder.exe_)可以通过CodedUI 测试生成器界面完成:[5]

上述用户界面可以在 CodedUI 测试项目中用于录制和生成实现上述测试用例的代码[5],首先点击“录制/暂停/继续”按钮,然后执行步骤 2 到 3,最后通过点击“生成代码”按钮生成测试代码,然后代码应如下所示(最好稍后改进 `SetFontSize` 方法,使字体大小成为输入参数):

CodedUITest 类

[TestMethod]
public void CodedUITestMethod1()
{
    // To generate code for this test, select "Generate Code for CodedUI Test" from the shortcut menu and select one of the menu items.
    this.UIMap.SetFontSize();
}

UIMap 类

/// <summary>
/// SetFontSize - Use 'SetFontSizeParams' to pass parameters into this method.
/// </summary>
public void SetFontSize()
{
    WinEdit uIFontEdit = this.UIAvalonEditSampleWindow.UIItemToolBar.UITxtFontEdit;
    WinEdit uIFontSizeEdit = this.UIAvalonEditSampleWindow.UIItemToolBar.UITxtFontSizeEdit;
   
    // Type '16' in 'FontSize' text box
    uIFontSizeEdit.Text = this.SetFontSizeParams.UIFontSizeEditText;

    // Click 'Font' text box
    Mouse.Click(uIFontEdit);
}

`UIMap` 文件是支持测试项目中的 UI 元素和操作的核心文件。测试项目至少包含 3 种类型的文件:`*.uitest`、`*.cs` 和 `*.designer.cs`。`*.uitest` 是一个 xml 文件。`*.uitest` 和 `*.designer.cs` 文件都是自动生成的(不应手动修改,因为下次录制操作后更改将丢失)。因此,要添加自定义代码,应双击 `*.uitest` 文件并单击“移动”按钮(下图中标有黄色)。

在单击“移动”按钮后,我们的示例中的 `RecordedMethod1` 位于 _UIMap.cs_ 文件中。我们可以将方法重命名为 `SetFontSize` 并添加一个参数。不用担心,CodedUI 测试生成器工具现在不会自动更改代码。请参阅常见问题解答部分以获取更多类似提示。

然后,当录制了初始测试步骤后,就该验证结果了。为此,只需将添加断言按钮拖到编辑器区域(编辑器区域会自动显示为蓝色,如下所示),以添加一个断言方法,该方法验证 AvalonEdit 中的文本字体是否已按预期更改。

可能的断言列表显示在添加断言工具窗口中。缺少字体大小属性。看来我们无法通过 CodedUI 测试生成器获取字体大小,因为 AvalonEdit 不会为 UIAutomation 公开字体属性。查看 AvalonEdit 的源代码,我们发现它实现了 `ITextRangeProvider` 接口,但在 `GetAttributeValue` 方法中没有返回任何有用的信息。

public object GetAttributeValue(int attribute)
{
    Log("{0}.GetAttributeValue({1})", ID, attribute);
    return null;
}

因此,为了使验证步骤可测试,我们可以细化 `GetAttributeValue` 方法以公开字体属性,或者在无法更改源代码的情况下,我们可以实现一个继承自要测试的控件的子类(此处要子类化的类是 TextEditor 类)。

应重写 `OnCreateAutomationPeer` 方法以返回一个自定义 AutomationPeer 子类,该子类实现相关的模式提供程序以公开相关属性。有关程序化访问桌面 UI 元素的更多信息,请参阅 UIAutomation 基础知识 [4]

在此示例中,`GetAttributeValue` 方法可以按如下方式细化(请参阅文章附带的下载示例或 GitHub 上的源代码:ICSharpCode.AvalonEdit\Editing\TextRangeProvider.cs):

public object GetAttributeValue(int attribute)
{
  if (AutomationTextAttribute.LookupById(attribute) == TextPatternIdentifiers.FontSizeAttribute)
  {
      return this.textArea.FontSize;
  }
  else
  {
    if (AutomationTextAttribute.LookupById(attribute) == TextPatternIdentifiers.FontNameAttribute)
      return this.textArea.FontFamily.Source;
    else
      return null;
  }
}

现在我们可以重新构建 AvalonEdit.Sample,再次拖动 assert 按钮,我们会发现 `Font` 属性获得了当前文本的字体名称值。仍然没有 `FontSize` 属性。Microsoft 提供了一种创建 CodedUI 测试框架自定义扩展的方法,该扩展可以支持特定的用户界面 [7]。但这很难调试,更糟糕的是,它已被 Microsoft 弃用。因此,在此不推荐此解决方案。

实际上,使用 UIAutomation 可以轻松直接访问 `FontSize` 属性。回到 FontSize 示例,在 CodedUI 测试项目中,手动添加以下验证方法以实现 `FontSize` 属性断言:

public void SetFontSize(double fontSize)
{
    this.UIAvalonEditSampleWindow.UIItemToolBar.UITxtFontSizeEdit.Text = fontSize.ToString();

    Mouse.Click(this.UIAvalonEditSampleWindow.UIItemToolBar.UITxtFontEdit);
}

public void AssertMethod1(double expectedFontSize)
{
    WpfControl uiTestControl = this.UIAvalonEditSampleWindow.UITextEditorDocument.UIItemDocument;

    AutomationElement automationElement = uiTestControl.NativeElement as AutomationElement;

    if (automationElement.TryGetCurrentPattern(TextPattern.Pattern, out object pattern))
    {
        object obj = ((TextPattern)pattern).DocumentRange.GetAttributeValue(TextPattern.FontSizeAttribute);
        double actualFontSize = Convert.ToDouble(obj);
        Assert.AreEqual(expectedFontSize, actualFontSize, "Font size verified.");
    }

    Assert.Fail("Assert fail message.");
}

`SetFontSize` 方法可用于在满足所有先决条件时更改测试目标的字体大小。另一方面,`AssertMethod1` 验证当前是否设置了给定的字体大小,如果未设置则引发“断言失败”。如下一节所示,可以使用类似的模式在测试方法中实现 `FontSize` get/set 方法。

更新测试方法并构建

用户交互的测试和正确结果的验证可以在 2 个单独的方法中实现。然后可以使用此代码实现完整的测试方法(请参阅下载示例 下载 AvalonEdit_05_03.CodedUITest.zip):

[TestMethod]
public void SetFontSizeTest()
{
    double expectedFontSize = 16;
    this.UIMap.SetFontSize(expectedFontSize);

    this.TestContext.WriteLine($"Step 1: set the font size to {expectedFontSize.ToString()}.");
    double actualFontSize = this.UIMap.GetFontSize();
    this.TestContext.WriteLine($"Step 2: get the current font size of the TextEditor: {actualFontSize}.");

    Assert.AreEqual(expectedFontSize, actualFontSize, "");

    this.TestContext.WriteLine("Step 3: confirm the current font size of the TextEditor is same as set in step 1.");
}

现在我们可以通过 Visual Studio 测试平台(通过 IDE)或通过命令行来构建和运行测试方法,测试应该能够成功执行。可以在 Visual Studio 中查看测试结果。

测试用例规范

TestMethod

标记为 `TestMethod` 属性的方法可以自动识别为测试用例。也可能存在其他与 `TestMethod` 相关的实例或方法。这些可能是初始化或构建使用 `TestContext` 执行的先决条件所必需的。本节将介绍初始化方法,并提供实现自定义 `TestContext` 的示例。

初始化方法

初始化方法可以在不同的执行级别实现。目前,有 4 种类型的初始化方法,按从上到下的粒度顺序排列:

  • `AssemblyInitialize`:仅在同一个程序集中的所有测试方法之前执行一次,
     
  • `ClassInitialize`:仅在同一个类中的所有测试方法之前执行一次,
     
  • `Constructor`:每次在执行测试方法之前执行(如果测试方法是实例方法,则首先创建一个新实例)
     
  • `TestInitialize`:每次在执行测试方法之前执行。

每个初始化方法都有一个清理方法(`Dispose` 方法可以视为构造函数的清理方法),并按相反的顺序执行。

实现自定义 TestContext

下面是一个示例,展示了一种实现和使用自定义 `TestContext` 来替换默认值的方法。

public class CustomTestContext : TestContext
{
    private TestContext testContext;
    public CustomTestContext(TestContext testContext) { this.testContext = testContext; }
    public override void WriteLine(string format, params object[] args)
    {
        // Perform custom behavior such as re-direct write stream.
    }
    
    public override DataRow DataRow => this.testContext.DataRow;
}

下一个代码示例展示了如何从测试内部使用 `TestContext` 实例。测试类实现了一个类型为 'TestContext' 且名称为 'TestContext' 的属性,名称和类型都不能更改,因为属性的 setter 会自动使用 MSTest Framework 在实际测试执行之前执行。

[TestClass]
public class Test
{
    [TestMethod]
    public void TestMethod1()
    {
    }

    private CustomTestContext testContext;

    public TestContext TestContext
    {
        get
        {
            return this.testContext;
        }
        
        set
        {
            this.testContext = new CustomTestContext(value);
        }
    }
}

任何测试都会受到许多全局变量的影响,我们已经在这里看到了 `TestContext` 如何在测试中使用。下一节将简要介绍测试配置文件,这些配置文件也应该在全局级别影响测试。

测试配置

Visual Studio 测试平台中有 2 种类型的配置文件:

  1. `.runsettings` 和
  2. `.testsettings`

`.runsettings` 文件用于配置单元测试 [8],而 `.testsettings` 文件则由 Visual Studio 2019 之前的版本使用。旧的 `.testsettings` 文件可以与 `.runsettings` 文件一起引用和使用,如下所示:

<RunSettings>
  <MSTest> 
    <SettingsFile>my.testsettings</SettingsFile>
    <ForcedLegacyMode>true</ForcedLegacyMode>
  </MSTest>
</RunSettings>

关于 CodedUI 的常见问题解答

请勿手动修改 *.Designer.cs 文件中的代码

CodedUI 测试生成器会生成 3 种文件。这些文件的扩展名为:

  1. `*.uitest`,
  2. `*.cs` 和
  3. `*.Designer.cs`.

_*.Designer.cs_ 文件在每次录制后都会更新。因此,您不应更改它,因为 Visual Studio 可能会覆盖您的更改。自定义方法应始终移动到 _*.cs_ 文件中。您可以通过双击 _*.uitest_ 文件并单击下图所示的“移动”按钮来执行此操作。

常见异常

通常由线程问题引起两种异常(`NullReferenceException` 和“无法找到 UI 控件”)。本节将解释该问题并提出解决方案。

NullReferenceException

CodedUI 测试以 COM(COM 对象模型)的单线程公寓(STA)模式运行。在此模式下,所有回放调用都应仅从 TestMethod 线程调用,并且 UITestControl 不应在 TestMethods 之间共享。

例如,`AssemblyInitialize` 和 `ClassInitialize` 方法是静态方法,它们在与 `TestMethod` 不同的线程中执行。因此,在调用任何 `UIMap` 方法之前,需要手动初始化回放环境并创建另一个 `UIMap` 实例。这通常可以通过这种 `try` 模式完成:

[ClassInitialize]
public static void MyClassInitialize() 
{     
    Playback.Initialize();     
    try     
    {         
         UIMap uiMap = new UIMap();
         uiMap.DoSomethin(); 
    }     
    finally     
    {         
        Playback.Cleanup();    
    } 
}

无法找到 UI 控件

此问题的详细错误消息为:“搜索可能在 'XXControl' 失败”

此错误可能由 CodedUI 测试生成器中的一个 bug 引起,该 bug 未录制目标控件的正确祖级。

自定义控件可能有虚拟化子控件。如果正在搜索的控件是 'XXControl' Custom 的后代,则将其包含为父容器可能会解决问题。

例如,如果有一个名为 A 的控件,其子控件名为 B,B 又有一个子控件名为 C。而 CodedUI 测试生成器未能检测到正确的树结构,并将 A 确定为 C 的容器。那么您需要手动更新结构来解决此问题,如下面的代码片段所示:

uiMap.A.C.Container = uiMap.B;

uiMap.B.Container = uiMap.A;

结论

本文试图将读者介绍给自动化 UI 测试的世界。我们不推荐黑魔法,而是建议采用从经验中学习到的模式,将 UI 测试集成到现有的开发周期中。我们还展示了如何通过继承自原始测试目标的自定义测试类来使一个似乎无法使用 UI 测试的属性变得可测试。

我们希望本文对感兴趣的读者有所帮助。如果您有其他问题或发现文章中有任何错误,请告知我们。

参考文献

  1. 不同类型的测试
    https://www.atlassian.com/continuous-delivery/software-testing/types-of-software-testing
     
  2. 等价类划分
    https://en.wikipedia.org/wiki/Equivalence_partitioning
     
  3. 边界值分析
    https://en.wikipedia.org/wiki/Boundary-value_analysis
     
  4. Microsoft UI Automation
    https://docs.microsoft.com/en-us/windows/desktop/WinAuto/entry-uiauto-win32
     
  5. 使用 CodedUI 测试来测试您的代码
    https://docs.microsoft.com/en-us/visualstudio/test/use-ui-automation-to-test-your-code?view=vs-2019
     
  6. UIAutomation 基础知识
    https://docs.microsoft.com/en-us/windows/desktop/WinAuto/entry-uiauto-win32
     
  7. 第三方控件的 CodedUI 测试扩展
    https://devblogs.microsoft.com/devops/coded-ui-test-extension-for-3rd-party-controls-the-basics-explained/
     
  8. 使用 .runsettings 文件配置单元测试
    https://docs.microsoft.com/en-us/visualstudio/test/configure-unit-tests-by-using-a-dot-runsettings-file?view=vs-2017
     

历史

在此处保持您所做的任何更改或改进的实时更新。

© . All rights reserved.