十六、自动测试

大多数开发人员通常认为单元和编码 UI 测试是应用项目生命周期中最单调的部分。 然而,提高单元测试代码覆盖率和创建自动化 UI 测试可以帮助节省大量开发人员的时间,否则这些时间将花费在维护和回归上。 特别是对于具有更长的生命周期的应用项目,项目的稳定性直接与测试自动化的水平相关。 本章将讨论如何创建单元和编码 UI 测试,以及围绕它们的架构模式。 数据驱动的单元测试、模拟和 Xamarin UI 测试是将要讨论的一些概念。

在本章中,我们将重点关注为应用生命周期的不同阶段创建不同类型的测试。 在了解在 Xamarin 的范围内设置单元测试和执行策略之前,我们将从单元测试开始本章。 然后,我们将转向集成测试和自动化 UI 测试,它们与单元测试一起,使开发人员能够在整个交付管道中检查他们的应用。

下面的主题将带你了解如何实现一个自动验证的应用开发管道:

  • 用测试维护应用的完整性
  • 使用集成测试维护跨模块的完整性
  • 自动化 UI 测试

在本章结束时,您将能够有效地使用 mock 和 fixture,同时创建战略准备的单元测试。 您还将了解集成和自动化测试背后的动机和好处。

用测试维护应用的完整性

本节的重点将放在单元测试以及用于 Xamarin 应用单元测试的策略和工具上。 我们将研究单元测试的基础知识,以及模拟和夹具的概念。

无论是开发平台还是运行时平台,单元测试都是开发管道中不可或缺的一部分。 事实上,如今,测试驱动开发(TDD)是最突出的开发方法,是任何敏捷开发团队的最佳选择。 在这个范例中,开发人员负责创建适合当前正在开发的单元的单元测试,甚至在编写实际业务逻辑实现的第一行之前就已经负责了。

安排,行动和断言

闲话少说,让我们看看应用中的第一个视图模型,并为它实现一些单元测试。 产品视图模型是一个简单的视图模型,在初始化时,它使用可用的服务客户端加载产品数据。 它暴露了两个属性; 即Items集合和ItemTapped命令。 利用这些信息,我们可以确定单位。

应用的单元可以通过实现简单的存根来识别,如下面的代码所示:

 public class ListItemViewModel : BaseBindableObject
 {
     public ListItemViewModel(IApiClient apiClient, INavigationService 
 navigationService)
     {
         //...Load products and initialize ItemTapped command
     }

     public ObservableCollection<ItemViewModel> Items { get; set; }

     public ICommand ItemTapped { get; }

     internal async Task LoadProducts()
     {
         // ...
         var result = await _serviceClient.RetrieveProductsAsync();
         // ...
     }

     internal async Task NavigateToItem(ItemViewModel viewModel)
     {
         // ...
     }
 }

我们的初始单元测试将为apiClient设置模拟,构造视图模型,验证RetrieveProductsAsync是否在服务客户端上被调用,并验证ItemTapped命令是否被正确初始化。 可以执行附加检查,以查看Items属性上是否触发了PropertyChanged事件。 在单元测试的上下文中,简单单元测试的这三个步骤通常被称为 AAA 或 AAA -Arrange, Act, Assert:

  1. Arrange部分中,准备一组结果数据,并使用模拟客户端返回数据:

    ```

    region Arrange

    var expectedResults = new List(); expectedResults.Add(new Product { Title = "testProduct", Description = "testDescription" }); // Using the mock setup for the IApiClient _apiClientMock.Setup(client => client.RetrieveProductsAsync()).ReturnsAsync(expectedResults);

    endregion

    ```

  2. 现在,让我们通过构造视图模型来执行Act步骤:

    ```

    region Act

    var listViewModel = new ListItemViewModel(_apiClientMock.Object);

    endregion

    ```

  3. 最后,对视图模型目标执行断言:

    ```

    region Assert

    // Just checking the resultant count as an example // Foreach with checking each expected product has a // matching domain entity would improve the robustness of the test. listViewModel.Items.Should().HaveCount(expectedResults.Count()); listViewModel.ItemTapped.Should().NotBeNull() .And.Subject.Should().BeOfType>(); _apiClientMock.Verify(client => client.RetrieveProductsAsync());

    endregion

    ```

  4. 现在运行单元测试来检查代码湾的愤怒:

图 16.1 -代码覆盖结果

有了这个简单的单元测试实现,我们已经达到了单元测试代码覆盖率的 80%。

重要提示

xUnit.net 框架被用来实现这个单元测试,或者所谓的事实。 此外,为了简化实现和断言,还使用了FluentAssertionsMoq框架。 这些框架的特性集超出了本书的范围。

实现足够好,可以检查构造函数的初始化。 我们测试的构造函数实现类似如下:

public ListItemViewModel(IApiClient apiClient)
{
    _serviceClient = apiClient;
    ItemTapped = new Command<ItemViewModel>(async _ => await 
    NavigateToItem(_));
    if (_serviceClient != null)
    {
        LoadProducts().ConfigureAwait(false);
    }
}

然而,请注意,LoadProducts方法实际上是在没有await的情况下调用的,因此它不会合并回初始同步上下文。 在多线程环境中,当并行执行多个单元测试时,可能会执行构造函数; 然而,在异步任务完成之前,断言就开始了。 这可以用穷人的线程同步——Task.DelayThread.Sleep来解决。

这个实现不过是一个临时的解决方案。 因为我们不能也不应该真正地在构造函数中等待任务完成,所以我们需要使用服务初始化模式:

public ListItemViewModel(IApiClient apiClient)
{
    _serviceClient = apiClient;
    ItemTapped = new Command<ItemViewModel>(async _ => await 
    NavigateToItem(_));
    if (_serviceClient != null)
    {
 (Initialized = LoadProducts()).ConfigureAwait(false);
    }
}
internal Task Initialized { get; set; }

现在,我们的Act实现看起来与类似:

#region Act
var listViewModel = new ListItemViewModel(_apiClientMock.Object);
await listViewModel.Initialized;
#endregion

重要提示

注意,我们无法在视图模型级别验证Items属性的PropertyChanged事件触发器。 其主要原因是ListItemsViewModel实例立即执行LoadProducts方法,甚至在我们有机会订阅目标事件之前,它的执行就已经完成了。 这也可以通过我们已经实现的模拟对象中的电路标志来补救,一旦监视器被附加到视图模型上,就释放任务。

要执行这些单元测试以及 IDE 扩展,请使用dotnet控制台命令:

dotnet test --collect "Code Coverage"

这个命令将执行可用的单元测试,并生成一个可以在 Visual Studio 中查看的覆盖率文件:

图 16.2 - DotNet 测试执行

在本节中,我们使用经典 AAA 设置为一个非常基本的视图模型创建单元测试。 然而,当视图模型对平台和应用服务都有大量依赖时,事情很容易变得复杂。 现在,我们已经回顾了单元测试的术语和基本单元测试知识,我们可以继续讨论 mock 了。

使用模拟创建单元测试

在实现单元测试时,隔离当前正在测试的单元是很重要的。 通过隔离,我们指的是模拟测试中当前主题的依赖关系的过程。 根据控制反转模式的实现,可以以各种方式引入这些模拟。 如果实现涉及构造函数注入,我们可以在测试的第一个 A 中模拟依赖接口,并将其传递给目标。 否则,像 NSubstitute 这样的框架可以替换接口,以及主体使用的具体类。

回顾一下我们的视图模型和我们实现的单元测试,您可能已经注意到我们使用 Moq 框架为我们的IApiClient对象创建了一个模拟接口实现。 现在,让我们学习如何使用 Moq 创建单元测试:

  1. 扩展构造函数,使其接受INavigationService实例,该实例将用于导航到所选项目的详细信息视图; 换句话说,隔离我们的ItemTapped命令的执行:

    public ListItemViewModel(IApiClient apiClient, INavigationService navigationService) { _serviceClient = apiClient; _navigationService = navigationService; ItemTapped = new Command<ItemViewModel>(async _ => await NavigateToItem(_)); if (_serviceClient != null) { (Initialized = LoadProducts()).ConfigureAwait(false); } }

  2. Our navigation command will be as follows:

    internal async Task NavigateToItem(ItemViewModel viewModel) { if (viewModel != null && _navigationService != null) { if (await _navigationService.NavigateToViewModel(viewModel)) { // Navigation was successful return; } } throw new InvalidOperationException("Target view model or navigation service is null"); }

    重要提示

    在本例中,我们将抛出一个异常,只是为了演示。 在实际实现中,更好的选择可能是在内部跟踪错误和/或仅在调试模式下抛出异常。 此外,抛出与这些场景中相同类型的异常也不完全是 SOLID。

  3. 现在,让我们实现我们的单元测试:

    [Trait("Category", "ViewModelTests")] [Trait("ViewModel", "ListViewModel")] [Fact(DisplayName = "Verify ListViewModel navigates on ItemTapped")] public async Task ListItemViewModel_ItemTapped_ShouldNavigateToItemViewModel() { #region Arrange _navigationServiceMock.Setup(nav => nav.NavigateToViewModel( It.IsAny<BaseBindableObject>())) .ReturnsAsync(true); var listViewModel = new ListItemViewModel( _apiClientMock.Object, _navigationServiceMock.Object); await listViewModel.Initialized; var expectedItemViewModel = new ItemViewModel() { Title = "Test Item" }; #endregion #region Act listViewModel.ItemTapped.Execute(expectedItemViewModel); #endregion #region Assert _navigationServiceMock.Verify( service => service.NavigateToViewModel(It.IsAny<ItemViewModel>())); #endregion }

  4. We have implemented the unit test to check the so-called happy path. We can also take this implementation one step further by checking whether the navigation service was called with expectedItemViewModel:

    Func<ItemViewModel, bool> expectedViewModelCheck = model => model.Title == expectedItemViewModel.Title; _navigationServiceMock.Verify( service => service.NavigateToViewModel( It.Is<ItemViewModel>(_ => expectedViewModelCheck(_))));

    为了弥补可能的结果(请记住,我们正在处理视图模型,就好像它是一个确定性有限自动机),我们将需要实现两个场景的导航服务nullnull命令参数,这两个将抛出InvalidOperationException

  5. Now, modify the Arrange section of the initial set:

    var listViewModel = new ListItemViewModel(_apiClientMock.Object, null);

    在这个特定的情况下,命令(即ICommand)是由异步任务(即NavigateToItem)构造的。 简单地在命令上调用Execute方法将吞下异常,这意味着我们将无法验证异常。

  6. Because of this, modify the execution so that it uses the actual view model method so that we can assert the exception:

    ```

    region Act

    // Calling the execute method cannot be asserted. // Action command = () => listViewModel.ItemTapped.Execute(expectedItemViewModel); Func command = async () => await listViewModel.NavigateToItem(expectedItemViewModel);

    endregion

    region Assert

    await command.Should().ThrowAsync();

    endregion

    ```

    注意,在这两个测试用例中,我们仍然使用相同的IApiClientmock,但没有设置方法。 我们仍然可以执行这个 mock,因为它是使用松散的 mock 行为创建的,它为集合返回类型返回一个空集合,而不是在没有进行适当设置的情况下为方法抛出异常。

  7. 这使得ListViewModel上的单元测试代码覆盖率达到了大约 90%,如下面的截图所示:

图 16.3 - Visual Studio 代码覆盖结果

到目前为止,所有的测试都已为视图模型实现。 根据定义,应用中的这些模块与 UI 和平台运行时是分离的。 如果我们要编写以 Xamarin 为目标的单元测试。 表单视图,或者目标视图模型需要运行时组件,运行时和运行时特性需要模拟,因为应用实际上不会在移动运行时上执行,而是在。net Core 运行时上执行。 Xamarin.Forms.Mocks包通过提供 Xamarin 的模拟运行时来填补这一空白。 表单视图可以初始化和测试。

夹具和数据驱动测试

正如您在我们之前实现的测试中可能已经注意到的,编写单元测试最耗时的部分之一是实现的安排部分。 在这一部分中,我们实际上设置了测试目标将使用的被测试系统。 在此设置中,我们的目标是将系统置于已知状态,以便将结果与预期结果进行比较。 这种已知状态也称为fixture

在这种背景下,固定可以简单模拟容器包含决定性的组件集,定义了被测系统(SUT),或推动的工厂可预测的行为模式。

**例如,如果我们要为我们的ListItemViewModel对象创建一个 SUT 工厂,我们可以通过向 fixture 注册两个依赖项来实现。 让我们开始:

  1. 通过初始化 fixture 并添加AutoMoqCustomization:

    _fixture = new Fixture(); _fixture.Customize(new AutoMoqCustomization());

    开始实现 2. 现在,为这两个服务接口设置模拟并冻结它们(也就是说,注册它们,使它们具有单例生命周期): 3. Now that the mocks have been set up, let's take a look at the Arrange block of our navigation test:

    ```

    region Arrange

    var listViewModel = _fixture.Create(); var expectedItemViewModel = _fixture.Create();

    endregion

    ```

    如我们所见,模拟接口注入已经由AutoMoqCustomization处理,并且已注册的冷冻样本用于实例。

    然而,如果我们用来执行测试目标的数据对象对结果的影响如此之大,以至于我们需要一个额外的测试用例,那么是什么呢? 例如,导航方法可以有两个不同的路径,这取决于视图模型中包含的数据:

    if (viewModel.IsReleased) { if (await _navigationService.NavigateToViewModel(viewModel)) { return; } } else { await _navigationService.ShowMessage("The product has not been released yet"); return; }

    在本例中,我们至少需要ItemViewModel对象的两种状态(即已释放和未释放)。 实现这一点最简单的方法是使用内联数据而不是夹具,使用提供的内联数据属性:

    [Trait("Category", "ViewModelTests")] [Trait("ViewModel", "ListViewModel")] [Theory(DisplayName = "Verify ListViewModel navigates on ItemTapped")] [InlineData(true, "Navigate")] [InlineData(false, "Message")] public async Task ListItemViewModel_ItemTapped_ShouldNavigateToItemViewModel( bool released, string expectedAction)

  2. 使用数据的内联提要,创建一个使用内联数据提要创建ItemViewModel数据项的编写器:

    var expectedItemComposer = _fixture.Build<ItemViewModel>() .With(item => item.IsReleased, released); var expectedItemViewModel = expectedItemComposer.Create();

  3. 现在,只需确保您验证了正确的navigationService方法被执行:

这样,ItemTapped命令的两种结果实际上都包含在单元测试中。

正如我们在 AAA 描述中看到的,单元测试仅仅是建立一个单元来测试它,并将其所有的依赖项隔离,执行单元,然后验证其结果。 尽管对于快节奏的项目来说,单元测试与模拟和 fixture 的结合可能看起来像是开销,但它可以提供一个有价值的基础。 单元测试是隔离模块的第一道防线。 然而,如果不检查这些模块如何一起工作,我们就会在应用中创建竖井。 下一节提供集成测试的见解。

用集成测试维护跨模块的完整性

大多数时候,当我们处理一个移动应用时,涉及到多个平台,例如作为客户端应用本身,可能是客户端应用的本地存储,以及多个服务器组件。 这些组件可以很好地以最健壮的方式实现,并通过单元测试具有深厚的代码覆盖率。 然而,如果这些组件不能一起工作,那么投入到单个组件中的工作将是徒劳的。

为了确保两个或多个组件能够很好地一起工作,开发人员可以实现端到端或集成测试。 虽然端到端场景通常由自动化 UI 测试覆盖,但集成测试是作为目标系统的一对排列来实现的。 换句话说,我们隔离了两个相互依赖的系统(例如,移动应用和 web API facade),并准备一个 fixture 来准备其余的组件,使它们处于已知状态。 一旦夹具为集成对做好了准备,集成测试的实现与单元测试的实现没有什么不同。

为了演示集成测试的价值,让我们看几个示例。

测试客户机-服务器通信

让我们假设我们有一套单元测试来测试客户端应用的视图模型。我们还实现了单元测试来控制IApiClient实现的完整性,而IApiClient实现是我们与服务层通信的主线。 在第一个套件中,我们将模拟IApiClient,而在后一个套件中,我们将模拟 HTTP 客户机。 在这两个套件中,我们涵盖了所有层,从核心逻辑实现一直到通过传输层发送请求。

此时,下一个业务顺序是编写使用IApiClient的实际实现的集成测试,该集成测试将向服务 API facade(也称为网关)发送服务请求。 然而,我们不能真正使用实际的网关部署,因为服务器端的多个模块将参与到这个通信中,而测试中的系统将太不可预测。

在这种情况下,我们有两个选择:

  1. 创建一个夹具控制器,该控制器将维护数据库和处于已知状态的其他活动部件(例如,将清理样本数据库并插入需要从中检索的数据的预测试执行)。
  2. 创建完整网关的临时部署,可能使用模拟模块作为依赖项,并在此系统上执行集成测试。

为了简单起见,让我们使用第一个选项,并假设部署了一个完全空的文档集合来运行集成测试。 在本例中,我们可以调整夹具,以便在预定的文档集合(即服务器端希望找到的文档集合)中注册一组产品,从应用客户机执行检索调用,并清理数据库。

我们将从实现我们的定制夹具开始:

public class DataIntegrationFixture : Fixture
{
    public async Task RegisterProducts(IEnumerable<Product> products)
    {
        var dbRepository = this.Create<IRepository<Product, string>>();
        foreach (var product in products)
        {
            await dbRepository.AddItemAsync(product);
        }
        this.Register(() => products);
    }
    public async Task Reset()
    {
        var dbRepository = this.Create<IRepository<Product, string>>();
        var items = this.Create<IEnumerable<Product>>();
        foreach (var product in items)
        {
            await dbRepository.DeleteItemAsync(product.Id);
        }
    }
}

我们有两种初始方法RegisterProductsReset:

  • RegisterProducts用于在夹具中插入检测数据和登记产品数据。
  • Reset用于清除插入的测试数据。 这样,测试执行将产生相同的结果——至少在数据库级别上是这样。 换句话说,测试的执行将是幂等的。

注意存储库是使用Create方法创建的,这样我们就可以将注入正确的存储库客户端的责任委托给测试计划。

现在,让我们开始进行测试:

  1. 首先创建测试初始化(即 xUnit 中的构造函数)和测试拆卸(即 xUnit 中的Dispose方法)。
  2. 在构造函数中,注册 fixture 将使用的存储库客户端实现,并注册使用此客户端的产品:

    public ClientIntegrationTests() { _fixture.Register<IRepository<Product, string>>(() => _repository); var products = _fixture.Build<Product>().With(item => item.Id, string.Empty).CreateMany(9); _fixture.RegisterProducts(products).Wait(); }

  3. 接下来实现IDisposable接口的Dispose方法。 这将是我们的测试拆卸功能:

    public void Dispose() { _fixture.Reset().Wait(); }

  4. 现在已经设置了初始化和拆卸过程,我们可以实现我们的第一个测试:

    [Fact(DisplayName = "Api Client Should Retrieve All Products")] [Trait("Category", "Integration")] public async Task ApiClient_GetProducts_RetrieveAll() { #region Arrange var expectedCollection = _fixture.Create<IEnumerable<Product>>(); #endregion #region Act var apiClient = new ApiClient(); var actualResultSet = await apiClient.RetrieveProductsAsync(); #endregion #region Assert actualResultSet.Should().HaveCount(expectedCollection.Count()); #endregion }

可以实现类似的测试来测试服务器与数据库或系统的其他组件之间的交互。 关键是控制未被测试的模块,并确保为目标交互执行测试。

实现平台测试

正如我们前面提到的,集成测试不一定是两个相互交互的独立运行时的断言。 它们还可以用于在受控环境中测试应用的两个不同的模块。 例如,在处理移动应用时,某些特性需要与移动平台交互(例如,本地存储 API 实现将使用本地平台文件系统; 甚至核心 SQLite 实现也被抽象为。net core)。

对于必须在特定移动平台(如 iOS、Android 和 UWP)上执行的集成测试,设备。 可以使用 xUnit 框架。 设备。 xUnit 框架由。net Foundation 管理。 作为 SDK 的一部分包含的多项目模板为目标平台和库项目创建测试工具项目。 一旦执行开始,测试将在提供真实或模拟目标平台的测试工具应用上执行,因此允许开发人员在特定于平台的特性上执行集成测试。

无论您是在测试模块之间的集成运行状况还是与外部服务的集成,集成测试都是交付管道中非常宝贵的成员。 在本节中,我们设计了一个示例测试场景,其中 API 客户机从一个远程服务检索数据,该服务具有由单元测试 fixture 控制的一组预先确定的数据。 虽然这种实现可以被理解为系统测试而不是集成,但完整的系统测试指的是在移动设备上执行测试而不隔离任何依赖关系的测试基础设施。 对于移动平台,自动化 UI 测试可以填补这一空白。

自动化 UI 测试

可以说,开发周期中最辛苦和最昂贵的阶段之一是人工认证测试,也称为验收测试。 在一个典型的非自动化验证周期中,认证测试所花费的时间可能比开发某个特性的时间长 2-3 倍。 此外,如果以前实现的特性存在风险,那么就必须执行这些区域的回归。 为了增加发布节奏并减少开发周期,实现自动化 UI(或端到端)测试是必要的。 通过这种方式,自动化管道可以验证一次,并重用来验证应用的 UI 和与其他系统的集成,而不是我们在每个发布周期中执行手动测试。

App Center 允许我们在几个实际设备上执行这些自动化测试,包括在开发管道中自动运行:

图 16.4 - App Center 测试结果视图

Xamarin 的。 UITests 是受支持的自动化框架中的一个,可用于创建这些自动化验收测试。

Xamarin。 UITests

Xamarin 的。 UITests 是一个自动化的 UI 测试框架,它与 Xamarin 目标平台紧密集成。 除了已经使用 Xamarin 框架创建的应用,它还可以用于为使用 Java 和 Objective-C/Swift 创建的移动应用创建自动化测试。 NUnit 与自动化框架一起用于执行断言和创建测试 fixture。

该框架允许开发人员使用查询和操作与移动平台进行交互。 查询可以描述为在IApp接口的当前实例上执行的select命令,而操作是与所选元素模拟的用户交互(即查询的结果)。 IApp接口使这种交互成为可能,它提供了目标平台之间所需的抽象,并促进了用户与它们的交互。

根据目标设备和平台,您可以以各种方式初始化IApp接口(换句话说,模拟交互平台)的实现。

以下是一些例子:

  • 初始化应用使用一个 iOS 应用 bundle 可以做如下:
  • 初始化它以运行在 iOS 模拟器与一个已经安装的应用可以做如下:
  • 对于当前连接到 ADB 的 Android 设备,可以按照以下步骤进行初始化:

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

一旦初始化了IApp实例,就可以使用前面提到的查询和操作执行模拟的用户交互。

可以使用各种可用的选择器编写查询。 最突出的查询如下:

  1. 标记:指 Xamarin 的x:Name。 元素,或者带有给定AutomationId对象的元素。 这与本地 UI 实现的方式类似,在 iOS 上使用AccessibilityIdentifiesAccessibilityLabel,在 Android 上使用一个视图的IdContentDescriptionText进行查询。
  2. :查询当前 UI 中指定的类名。 它通常与nameof(MyClass)一起使用。
  3. Id:这指的是我们试图定位的元素的Id部分。
  4. Text:包含给定文本的任何元素。

例如,如果我们要点击一个标记为ProductsView的元素并选择列表中的第一个子元素,我们将使用以下代码:

app.Tap(c => c.Marked("ProductsView").Class("ProductItemCell").Index(0));

重要的是要注意查询的流畅执行风格,其中每个查询返回一个AppQuery对象,而应用操作使用Func<AppQuery, AppQuery>委托。

为某个视图创建结构化查询的最简单的方法是使用 Xamarin 提供的Read-Eval-Print-Loop(REPL)。 UITests 框架。 要启动 REPL,你可以使用相关的IApp方法:

app.Repl();

在终端会话上初始化 REPL 之后,tree命令可以提供完整的视图树。 您还可以使用相同的IApp实例执行应用查询和操作:

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"
>>>

操作因所选视图元素的不同而不同,但最常用的操作如下:

  1. Tap:用于模拟用户的点击手势。
  2. EnterText:将文本输入到所选视图中。 需要注意的是,在 iOS 上,软键盘用于输入文本,而在 Android 上,数据直接传递到目标视图。 当您与隐藏在键盘下或被键盘偏移的元素交互时,这可能会导致问题。
  3. WaitForElement:等待查询定义的元素出现在屏幕上。 有时,使用较短的超时时间,可以将此方法用作元素断言的一部分。
  4. Screenshot:这是给定标题的截图。 这表示 App Center 执行中的一个步骤。

页面对象模式

在某个测试方法中实现 UI 测试可能会变得相当乏味。 事实上,自动化平台的查询和操作将变得紧密耦合且不可维护。 为了避免这种情况,建议使用页面对象模式(POP)。

在 POP 中,屏幕上的每个视图或不同的视图元素实现其自己的页面类,该类实现与该特定页面的交互,以及该页面内视图组件的选择器。 这些交互是以一种简化的、词法的方式实现的,因此后台的复杂自动化实现不会反映在实际的测试实现中。 此外,对于交互和查询,页面对象还负责提供一种导航到另一个页面和从另一个页面导航的方法。

让我们学习如何实现我们的 POP 结构:

  1. Let's start by creating our BasePage object:

    ``` public abstract class BasePage where TPage : BasePage { protected abstract PlatformQuery Trait { get; }

    public abstract TPage NavigateToPage();
    
    internal abstract Dictionary<string, Func<AppQuery, AppQuery>> 
    Selectors { get; set;}
    
    protected BasePage() {}
    
    // ,..
    // Additional Utility Methods for ease of execution
    

    } ```

    基类规定每个实现都应该实现一个定义页面本身(以验证应用已导航到目标视图)的Trait对象和一个将用户(从主屏幕)带到实现视图的导航方法。

  2. Now, let's implement a page object for the About view:

    ``` public class AboutPage : BasePage { public AboutPage() { Selectors = new Dictionary>() Selectors.Add("SettingsMenuItem", x => x.Marked("Settings")); Selectors.Add("SettingsMenu", x => x.Marked("CategoryView")); Selectors.Add("AboutPageMenuItem", x => x.Marked("Information")); Selectors.Add("Title", x => x.Marked("Title")); Selectors.Add("Version", x => x.Marked("Version")); Selectors.Add("PrivacyPolicyLink", x => x.Marked("PrivacyPolicyLink")); Selectors.Add("TermsOfUseLink", x => x.Marked("TermsOfUseLink")); Selectors.Add("Copyright", x => x.Marked("Copyright"); } internal override Dictionary> Selectors { get; set;} protected override PlatformQuery Trait => new PlatformQuery { Android = x => x.Marked("AboutPage"), iOS = x => x.Marked("AboutPage") }; public override AboutPage NavigateToPage() { // Method implemented in the base page using the App OpenMainMenu();

        App.WaitForElement(Selectors["SettingsMenuItem"], 
                "Timed out waiting for 'Settings' menu item");
    
        App.Tap(Selectors["SettingsMenuItem"]);
        App.WaitForElement(Selectors["SettingsMenuItem"], 
                "Timed out waiting for 'Settings' menu");
    
        App.Screenshot("Settings menu appears.");
        App.Tap(Selectors["AboutPageMenuItem"]);
        if(!App.Query(Trait).Any())
        {
            throw new Exception("Navigation Failed");
        }
        App.Screenshot("About page appears.");
        return this;
    }
    public AboutPage TapOnTermsOfUseLink()
    { 
         App.WaitForElement(Selectors["TermsOfUseLink"], 
                "Timed out waiting for 'Terms Of Use' link");
         App.Tap(Selectors["TermsOfUseLink"]);
         App.Screenshot("Terms of use link tapped");
    
         return this;
    }
    

    } ```

    因此,现在,使用AboutPage实现并在AboutPage上执行操作就像初始化Page类并导航到它一样简单:

    new AboutPage() .NavigateToPage() .TapOnTermsOfUseLink()

对于是否在页面中包含断言,或者仅仅公开选择器,以便将断言作为测试的一部分实现,社区存在分歧。 无论如何,实现 POP 都可以帮助开发人员和 QA 团队在短时间内轻松地创建易于维护的测试。

总结

在本章中,我们研究了自动化测试和验证过程的各种测试策略。 创建自动化测试可以帮助我们控制在开发生命周期中创建的技术债务,并保持对源代码的检查,从而提高代码和管道本身的质量。 如您所见,其中一些测试与单元测试一样简单,单元测试是在应用生命周期的开始阶段实现的,并且几乎在每个代码检查点执行,而有些测试是复杂的,例如集成和编码 UI 测试, 它们通常是在开发阶段的末尾编写的,并且只在特定的检查点执行(即,每夜构建或预发布检查)。 无论如何,目标应该始终是为代码创建一个可认证的管道,而不是为认证创建代码。

随着测试的覆盖,我们可以说我们的交付管道的持续集成部分被覆盖了。 接下来,我们将进入持续交付阶段,在此阶段,我们将通过使用 Azure Resource Manager 来管理所需状态,将基础设施作为代码来关注。**