Xamarin.UITest

重要

Visual Studio App Center 计划于 2025 年 3 月 31 日停用。 虽然可以继续使用 Visual Studio App Center,直到它完全停用,但你可以考虑迁移到几个建议的替代方法。

详细了解支持时间线和替代方案。

Xamarin.UITest 是一个 C# 测试框架,使用 NUnit 进行 iOS 和 Android 应用中的 UI 验收测试。 它与 Xamarin.iOS 和 Xamarin.Android 项目紧密集成,但也可用于本机 iOS 和 Android 项目。 Xamarin.UITest 是允许 NUnit 测试在 Android 和 iOS 设备上执行的 自动化库 。 测试与用户界面交互,就像用户一样:输入文本、点击按钮和手势(如轻扫)。

通常,每个 Xamarin.UITest 都编写为称为 [Test]的方法。 包含测试的类称为 [TestFixture]。 测试装置包含单个测试或一组测试。 固定装置还负责设置,使测试运行并在测试完成时完成清理。 每个测试都应遵循 Arrange-Act-Assert 模式:

  1. 排列:测试将设置条件并初始化内容,以便可以执行测试。
  2. 操作:测试将与应用程序交互、输入文本、按下按钮等。
  3. 断言:测试检查在 Act 步骤中运行的操作的结果,以确定正确性。 例如,应用程序可能会验证是否显示特定的错误消息。

开始使用 Xamarin.UITest 的最佳时机是在移动应用程序开发期间。 自动测试是在根据以下列表中所述的步骤开发的一项功能时编写的:

  1. 在 Android 或 iOS 应用程序中开发该功能。
  2. 编写测试并在本地运行以验证功能。
  3. 在 App Center 测试中创建新的测试运行,或使用现有的测试运行。
  4. 编译 IPA 或 APK,然后将其与测试一起上传到 App Center 测试。
  5. 修复 App Center 测试公开的任何问题或 bug。
  6. 通过转到应用程序的下一个功能来重复此过程。

对于不再处于活跃开发状态的现有应用程序,以追溯方式添加自动测试可能不经济高效。 相反,更好的方法是在修复 bug 时使用 Xamarin.UITest。 例如,假设某个应用程序没有自动测试,并且用户报告了 bug。 分配用于修复该 bug 的开发人员可能需要执行一些 (或全部) 以下操作:

  • 手动验证 bug 或回归。
  • 使用演示 bug 的 Xamarin.UITest 编写测试。
  • 将测试提交到 App Center 测试,以深入了解 Bug 的范围和对相关设备的影响。
  • 修复 bug。
  • 使用传递的 Xamarin.UITest 证明 bug 已修复。
  • 提交修补程序并测试到 App Center 测试,以验证是否已在相关设备上修复该 bug。
  • 将传递的测试检查到版本控制中。

自动化 UI 测试在很大程度上依赖于查找屏幕上的视图并与之交互。 Xamarin.UITest 通过两组相互协作的重要 API 满足此要求:

  1. 可在视图上执行的操作 - Xamarin.UITest 提供了允许测试模拟常见用户操作(如点击视图、输入文本或轻扫视图)的 API。
  2. 用于在屏幕上查找视图的查询 - Xamarin.UITest 框架的一部分是将在屏幕上查找视图的 API。 查询通过检查视图的属性并返回操作可能处理的对象,在运行时查找视图。 以这种方式查询是一种强大的技术,它允许为用户界面编写测试,无论屏幕大小、方向或布局如何

为了帮助编写测试,Xamarin.UITest 提供了 read-eval-print-loop (REPL) 。 REPL 允许开发人员和测试人员在应用程序运行时与屏幕交互,并简化了查询的创建。

Xamarin.UITest API 简介

与移动应用程序的所有测试交互都通过 实例 Xamarin.UITest.IApp进行。 此接口定义了测试与应用程序协作以及与用户界面交互的关键方法。 此接口有两个具体实现:

  • Xamarin.UITest.iOS.iOSApp 此类将针对 iOS 自动执行测试。
  • Xamarin.UITest.Android.AndroidApp 此类适用于在 Android 上自动执行测试。

iOSAppAndroidApp 对象不会直接实例化。 而是使用帮助程序 ConfigureApp 类创建它们。 此类是一个生成器,可确保 iOSApp 正确实例化 或 AndroidApp

建议对每个测试使用新 IApp 实例。 新实例可防止状态从一个测试溢出到另一个测试。 NUnit 测试可在两个位置初始化 实例 IApp

  • SetUp在 方法中,测试固定装置是相关测试的逻辑分组,每个测试都独立于另一个测试运行。 在这种情况下, IApp 应在 方法中 SetUp 初始化 ,确保每个测试都有新的 IApp 可用。
  • TestFixtureSetup在 方法中,在某些情况下,单个测试可能需要其自己的测试固定装置。 在这种情况下,在 方法中TestFixtureSetup初始化IApp对象一次可能更有意义。

配置后 IApp ,测试可能会开始与所测试的应用程序交互。 为此,需要获取对屏幕上可见的视图的引用。 Xamarin.UITest 中的许多方法都采用参数 Func<AppQuery, AppQuery> 来查找视图。 例如,以下代码片段演示如何点击按钮:

app.Tap(c=>c.Button("ValidateButton"));

Xamarin.UITest 框架中有两个接口实现 IApp ,一个用于 iOS,一个用于 Android。

初始化 iOS 应用程序的 IApp

当 Xamarin.UITest 在 iOS 上运行测试时,它会启动 iOS 模拟器的实例、部署应用程序、启动应用程序并开始运行测试。 iOS 应用程序必须已生成。 Xamarin.UITest 不会编译应用程序,也不会为你创建应用捆绑包。

方法 AppBundle 可用于指定可在文件系统上找到应用捆绑包的位置。 可通过两种方法实现此目的:绝对路径或相对路径。 此代码片段演示如何使用应用捆绑包的绝对路径:

IApp app = ConfigureApp
    .iOS
    .AppBundle("/path/to/iosapp.app")
    .StartApp();

分部路径必须相对于 Xamarin.UITest 程序集。 此代码片段是一个示例:

IApp app = ConfigureApp
    .iOS
    .AppBundle("../../../iOSAppProject/bin/iPhoneSimulator/Debug/iosapp.app")
    .StartApp();

相对路径示例指示 AppBundle 从 Xamarin.UITest 程序集上调三个目录,然后向下导航 iOS 应用程序项目的项目树以查找应用捆绑包。

ConfigureApp 确实具有其他方法来帮助配置 IApp。 有关更多详细信息,请参阅 iOSAppConfigurator 类。 下表介绍了一些更有趣的方法:

方法 说明
AppBundle 此方法指定测试时要使用的应用捆绑包的路径。
Debug 此方法将在测试运行器中启用调试日志记录消息。 此方法可用于排查在模拟器上运行应用程序时出现的问题。
DeviceIdentifier 将设备配置为与设备标识符一起使用。 下面将更详细地介绍此方法。
EnableLocalScreenshots 在本地运行测试时启用屏幕截图。 当测试在云中运行时,始终启用屏幕截图。

有关如何在特定 iOS 模拟器上运行 iOS 测试的详细信息,请参阅 确定 iOS 模拟器的设备 ID

初始化适用于 Android 应用程序的 IApp

Xamarin.UITest 会将现有 APK 部署到已运行的附加设备或 Android 模拟器的实例。 应用将启动,然后运行测试。 Xamarin.UITest 无法生成 APK,也无法启动 Android 模拟器的实例。

ApkFile的 方法IApp用于指定在文件系统上可以找到 APK 的位置。 可通过两种方法实现此目的:绝对路径或相对路径。 此代码片段显示使用 APK 的绝对路径:

IApp app = ConfigureApp
    .Android
    .ApkFile("/path/to/android.apk")
    .StartApp();

分部路径必须相对于 Xamarin.UITest 程序集。 此代码片段是一个示例:

IApp app = ConfigureApp
    .Android
    .ApkFile("../../../AndroidProject/bin/Debug/android.apk")
    .StartApp();

相对路径示例指示 ApkFile 从 Xamarin.UITest 程序集中向上浏览三个目录,然后向下导航 Android 应用程序项目的项目树以查找 apk 文件。

如果连接了多个设备或仿真器,Xamarin.UITest 将停止测试执行并显示错误消息,因为它无法解析测试的预期目标。 在这种情况下,需要提供设备或仿真器的 串行 ID 才能运行测试。 例如,考虑命令的以下输出 adb devices ,其中列出了) 连接到计算机的所有设备 (或仿真器 (及其串行 ID) :

$ adb devices
List of devices attached
192.168.56.101:5555 device
03f80ddae07844d3    device

可以使用 方法指定 DeviceSerial 设备:

IApp app = ConfigureApp.Android.ApkFile("/path/to/android.apk")
                               .DeviceSerial("03f80ddae07844d3")
                               .StartApp();

与用户界面交互

为了与视图交互,许多 IApp 方法采用 Func<AppQuery, AppQuery> 委托来查找视图。 此委托使用 AppQuery Xamarin.UITest 查找视图的方式的核心。

AppQuery 是用于生成查询以查找视图的 流畅接口 。 在提供 的方法中 AppQueryMarked 方法是最简单、最灵活的方法之一。 此方法使用启发式方法来尝试查找视图,将在下一部分中更详细地讨论。 目前,请务必了解 具有 IApp 许多与应用程序交互的方法。 这些方法使用 Func<AppQuery, AppQuery> 获取对要与之交互的视图的引用。 下面列出了 提供的 AppQuery 一些更有趣的方法:

方法 说明
Button 将在屏幕上找到一个或多个按钮。
Class 将尝试查找属于指定类的视图。
Id 将尝试查找具有指定 ID 的视图。
Index . 将从匹配的视图集合中返回一个视图。 通常与其他方法结合使用。 采用从零开始的索引。
Marked 将根据下面讨论的启发法返回视图。
Text 将匹配包含所提供文本的视图。
TextField 将匹配 Android EditText 或 iOS UITextField

例如,以下方法演示如何模拟点击名为“SaveUserdataButton”的按钮:

app.Tap(c=>c.Marked("SaveUserDataButton"));

由于 AppQuery 是一个 Fluent 接口,因此可以将多个方法调用链接在一起。 请考虑点击视图的这个更复杂的示例:

app.Tap(c=>c.Marked("Pending")
            .Parent()
            .Class("AppointmentListCell").Index(0));

在这里, AppQuery 将首先查找标记为 Pending的视图,然后选择该视图的第一个 AppointmentListCell 父级,该视图的类型为 。

尝试通过查看移动应用来创建这些查询可能很棘手。 Xamarin.UITest 提供了一个 REPL,可用于浏览屏幕的视图层次结构、尝试创建查询,以及使用它们与应用程序交互。

使用 REPL

启动 REPL 的唯一方法是在现有测试中调用 IApp.Repl 方法。 这需要创建 NUnit TestFixture,配置可在 方法中使用的 Test 实例IApp。 以下代码片段演示了如何执行此操作的示例:

[TestFixture]
public class ValidateCreditCard
{
    IApp app;

    [SetUp]
    public void Setup()
    {
        app = ConfigureApp.Android.ApkFile("/path/to/application.apk").StartApp();
    }
    [Test]
    public void CreditCardNumber_TooLong_DisplayErrorMessage()
    {
        app.Repl();
    }
}

若要运行测试,请右键单击 Visual Studio 的装订线并选择“ 运行”:

包含测试运行选项的弹出菜单的屏幕截图

测试将运行,调用 方法时 Repl ,Xamarin.UITest 将在终端会话中启动 REPL,如以下屏幕截图所示:

运行 Xamarin.UITest REPL 的 macOS 终端的屏幕截图

REPL 已初始化名为 app的 实例,该实例IApp与应用程序交互。 首先要做的一件事是浏览用户界面。 REPL 有一个 tree 命令来执行此操作。 它将在显示的屏幕中打印出视图的层次结构。 例如,请考虑以下应用程序的屏幕截图:

iPhone 上运行的示例应用程序的屏幕截图

可以使用 tree 命令显示此屏幕的以下层次结构:

App has been initialized to the 'app' variable.
Exit REPL with ctrl-c or see help for more commands.

>>> tree
[UIWindow > UILayoutContainerView]
  [UINavigationTransitionView > ... > UIView]
    [UITextView] id: "CreditCardTextField"
      [_UITextContainerView]
    [UIButton] id: "ValidateButton"
      [UIButtonLabel] text: "Validate Credit Card"
    [UILabel] id: "ErrorrMessagesTestField"
  [UINavigationBar] id: "Credit Card Validation"
    [_UINavigationBarBackground]
      [_UIBackdropView > _UIBackdropEffectView]
      [UIImageView]
    [UINavigationItemView]
      [UILabel] text: "Credit Card Validation"
>>>

我们可以看到,此视图中有一个 UIButton 具有 idValidateButton 的 。 可以使用 命令显示 tree 的信息来帮助创建必要的查询来查找视图并与之交互。 例如,以下代码模拟点击按钮:

app.Tap(c=>c.Marked("ValidateButton"))

输入命令时,REPL 会记住缓冲区中的命令。 REPL 提供一个 copy 命令,用于将此缓冲区的内容复制到剪贴板。 这使我们能够创建测试的原型。 可以使用 将 REPL 中完成的工作复制到剪贴板 copy,然后将这些命令粘贴到 中 [Test]

使用标记查找视图

AppQuery.Marked 方法是一种方便而强大的方法来查询屏幕上的视图。 它的工作原理是检查屏幕上视图的视图层次结构,尝试将视图上的属性与提供的字符串匹配。 Marked 工作原理因操作系统而异。

查找已标记的 iOS 视图

iOS 视图将使用以下属性之一进行定位:

  • AccessibilityIdentifier视图的
  • AccessibilityLabel视图的

例如,请考虑以下 C# 代码片段,该代码片段创建 UILabel 并设置 AccessibilityLabel

UILabel errorMessagesTextField = new UILabel(new RectangleF(10, 210, 300, 40));
errorMessagesTextField.AccessibilityLabel = "ErrorMessagesTextField";
errorMessagesTextField.Text = String.Empty;

可以通过以下查询找到此视图:

AppResult[] results = app.Marked("ErrorMessagesTextField");

查找已标记的 Android 视图

Android 视图将基于以下属性之一进行定位:

  • Id视图的
  • ContentDescription视图的
  • Text视图的

例如,假设 Android 布局定义了以下按钮:

<Button
    android:text="Action 1"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:id="@+id/action1_button"
    android:layout_weight="1"
    android:layout_marginLeft="5dp" />

我们可以看到 android:id ,此按钮的 action1_buttonandroid:text操作 1。 以下两个查询之一将在屏幕上找到按钮:

  • app.Query(c=>c.Marked("action1_button"));
  • app.Query(c=>c.Marked("Action 1"));

使用 Xamarin.UITest.IApp 控制应用程序

配置并初始化后 IApp ,测试可能会开始与应用程序交互。 使用 Func<AppQuery, AppQuery> 的方法的一个示例是 IApp.Query() 方法。 此方法将执行查询并返回结果。 最简单的示例显示在以下代码片段中,该代码片段返回屏幕上可见的所有视图的列表:

AppResult[] results = app.Query(c=>c.All())

下表演示了使用 AppQuery 查找屏幕上视图的一些其他示例:

语法 结果
app.Query(c=>c.Class("UILabel")) 方法 .Class() 将查询作为 iOS UILabel的子类的视图。
app.Query(c=>c.Id("txtUserName")) 方法 .Id() 将查询具有 IdtxtUserName 的 的视图。
app.Query(c=>c.Class("UILabel").Text("Hello, World")) 查找文本为“Hello, World”的所有 UILabel 类。
results = app.Query(c=>c.Marked("ValidateButton")) 返回用指定文本 标记 的所有视图。 方法是 Marked 一个有用的方法,可以简化查询。 下一部分将介绍此内容。

下表列出了一些 (但并非所有) ,这些方法 IApp 可用于与屏幕上的视图交互或操作:

示例 说明
PressEnter 在应用中按 Enter 键。
Tap 模拟匹配元素上的点击/触摸手势。
EnterText 在视图中输入文本。 在 iOS 应用程序中,Xamarin.UITest 将使用软键盘输入文本。 相比之下,Xamarin.UITest 不会使用 Android 键盘,它会直接将文本输入到视图中。
WaitForElement 暂停测试的执行,直到视图显示在屏幕上。
Screenshot(String) 获取处于当前状态的应用程序的屏幕截图,并将其保存到磁盘。 它返回一个 对象, FileInfo 其中包含有关所拍摄屏幕截图的信息。
Flash 此方法将导致所选视图在屏幕上“闪烁”或“闪烁”。

有关 接口的详细信息IApp,请参阅 、 AndroidAppiOSAppIAppAPI 文档

作为如何使用这些方法的示例,请考虑对上面显示的屏幕截图进行以下测试。 此测试将在文本字段中输入 17 位数的信用额度卡,然后点击屏幕上的按钮。 然后,它会检查屏幕中是否有错误消息,告知用户该号码太长,无法成为有效的信用额度卡号码:

[Test]
public void CreditCardNumber_TooLong_DisplayErrorMessage()
{
    /* Arrange - set up our queries for the views */
    // Nothing to do here, app has been instantiated in the [SetUp] method.

    /* Act */
    app.EnterText(c => c.Marked("CreditCardTextField"), new string('9', 17));
    // Screenshot can be used to break this test up into "steps".
    // The screenshot can be inspected after the test run to verify
    // the visual correctness of the screen.
    app.Screenshot("Entering a 17 digit credit card number.");

    app.Tap(c => c.Marked("ValidateButton"));
    app.Screenshot("The validation results.");

    /* Assert */
    AppResult[] result = app.Query(c => c.Class("UILabel").Text("Credit card number is too long."));
    Assert.IsTrue(result.Any(), "The error message isn't being displayed.");
}

此测试还使用 Screenshot 方法在测试执行期间的关键点拍摄照片。 运行此测试时,App Center 将获取屏幕截图并将其显示在测试结果中。 方法允许将测试分解为步骤并提供屏幕截图的说明。