使用页面对象模型简化iOS UI测试

为什么还要编写测试呢?

让我们从基础开始,为什么我们甚至需要编写测试?

即使我是编写测试的坚定支持者,但我确实理解有时候由于各种原因,没有足够的时间或资源可以专门用于测试,最常见的情况是缺乏对重要功能的理解,或者换句话说,缺乏公司文化。

通常是我们开发人员必须说服我们的经理我们需要编写测试。
另一方面,有很多开发人员根本不相信编写测试,或者只是出于某种原因不喜欢编写测试。
如果您是这些开发人员或管理人员之一,我将请您仔细考虑以下事实。

在医院洗手是相当近期的做法。

直到1847年,匈牙利医生Ignaz Semmelweis才提出医护人员洗手以降低医院死亡率的想法。

他的想法与当时的科学观点和医学观点相矛盾,并被当时的医学界完全拒绝。
由于遭到拒绝和嘲笑,他感到神经衰弱,并在庇护中死亡。
医学界花了很长时间才开始将洗手作为强制性做法。
毋庸置疑,从那时起,洗手挽救了无数生命,而提到的医生是现代英雄。

杀手bug

我希望我能说,没有人为了让我们(软件开发人员)能够学习到有关编写测试的重要性的类似课程而死,但这已经与臭名昭著的计算机控制放射治疗机Therac的 “杀手臭虫”发生了-25 ,这会导致它发生故障,并给至少6名患者带来大量(有时甚至是致命的)辐射剂量。

如果以适当的方式对Therac-25的软件进行了测试,也许可以避免发生这一系列可怕的悲剧。

测试iOS应用

软件开发社区中有关自动测试的情况看起来像19世纪后期的医学界,在iOS应用程序开发社区中尤其如此。

编写任何类型的测试通常都是iOS应用开发的事后思考,特别是在较小的团队中。 实际上具有大量单元测试的某些项目通常会完全省略UI测试。
作为iOS开发人员,我们的工作与其他软件开发人员没有任何不同,我们应该像其他开发人员一样坚持经过时间验证的做法,并通过测试彻底覆盖我们的代码。
我们可以使用的某些工具可能还不如XCUITest (我在看着你! ),但这并不意味着我们不能在它们之上构建并解决眼前的问题,毕竟,这就是我们要做的正在付款。

在BUX上进行测试

当我开始在BUX工作时,我被分配给一个经验丰富的iOS开发人员团队,他们准备开始构建具有挑战性的新iOS应用程序。
团队已经使用当前的BUX应用程序设置了很高的质量标准,该应用程序已经在App Store上运行了几年,并且非常稳定,有成千上万的用户,几乎没有崩溃。
毋庸置疑,这种稳定性是严格的测试程序的产物,我们也在新应用程序的开发过程中加以应用和改进。

新应用,新挑战

我们正在构建的新应用是下一代股票交易应用。
它是从头开始设计的,其任务是抽象出股票交易的复杂性,将其带给以前从未交易过的人们,同时使用户界面尽可能直观,并为其添加独特的社交组件。

有很多重叠的实时事件,例如股票报价更新,账户余额更新,交易执行,社交事件等,它们可以在一瞬间发生并动态影响用户界面,有时一次会发生多个事件。 另外,可以从应用程序中的多个点触发很多自定义流。

我们的解决方案

为了降低复杂性,我们将应用程序分为几个模块/框架,一个包含核心业务逻辑,一个用于发出和发送http请求,另一个用于Web套接字事件观察,等等。
通过使用单元测试,测试这些框架非常简单,单元测试覆盖了它们的100%。

当要测试应用程序本身时,出现了一个大问题。
测试应用程序的某些部分非常简单,我们的逻辑控件已从其支持的视图中分离出来,因此我们能够通过使用单元测试轻松地涵盖其中的大部分内容。

另一方面,端到端测试所有UI流程并不是那么简单。
手动测试它们会花费时间,消耗人力并且容易出错。
另一方面,使用XCUITest进行自动UI测试并不是有史以来最好的开发人员体验。
有一些出色的iOS UI测试框架可用,但是我们觉得其中大多数都不适合我们,或者仅仅是一个过大的选择,因此我们决定构建自己的简单且最小的iOS UI测试框架。

我们正在尝试做的是通过控制应用程序接收的所有数据,以一种用户使用应用程序的方式来模拟应用程序的使用,但是在受控环境中。
在单元测试的情况下,通过模拟和存根实现非常简单,但是在UI测试的情况下,则变得更加复杂,因为您没有相同级别的访问应用程序代码的权限。
iOS App及其UI测试本质上是两个独立的过程,它们进行通信的唯一方法是在测试开始时通过XCUIApplication变量传递一些参数。
有一些第三方解决方案即使在应用程序和UI测试目标已经运行后也能在它们之间传递数据,但是我们认为这太过hacky和容易出错,因此我们围绕此约束构建了框架。

页面对象模型

我们的解决方案基于Page Object Model( 这里是Martin Fowler的很好解释 )模式,结合了残存的HTTP响应,模拟的Web套接字事件以及一种使我们能够进行阻塞,同步断言的机制。

我们在每个测试案例的开始都设置了残存的HTTP响应,这是通过使用令人惊叹的OHHTTPStubs框架实现的 。 对于Web套接字事件模拟,事情并非那么容易完成,因此我们必须在UI测试目标中创建一个运行于应用程序连接到UI测试目标时运行的小型Web服务器,然后我们才能直接从中发出事件在UI测试目标中相应地满足我们的需求。 阻止同步断言是通过预先存在的XCUITest方法实现的。
在本文中,我将仅详细介绍与POM相关的组件,而我们的UI测试系统的其他3个组件将在以后的文章中介绍。

每个屏幕都有自己的页面,也可以有子页面,例如。 具有表格视图的屏幕将是其自己的页面,同时包含与其包含的单元格数量一样多的子页面,并且每个页面都将具有一定程度的交互作用(点击,滑动,检测是否在屏幕上)等)。

Page只是XCUIElement周围的一个简单包装器:

反过来, Page被包装在UIElementPage中,该UIElementPage提供对特定UI元素的访问:

您还可以查看访问不同类型元素的示例。

UIElement是围绕accessibilityIdentifier的简单包装协议:

为了使视图易于添加到可访问性层次结构中,我们设计了一个简单的协议UITestablePage

UITestablePage还包含一种用于使视图的连续类型(例如,表格单元格)可测试的方法。

我们所有的UI元素都被组织为一个枚举UIElements其中包含页面,单元格,对话框,警报等的子枚举。
我们的应用程序中包含不同类型的页面的一些示例:

如您所见,页面通常具有根,它基本上是视图控制器本身的视图(或在不同页面的情况下为其他视图),以及一些其他元素,例如按钮,表格等。
在视图控制器方面,其用法如下所示:

首先,我们为UIElements设置足够的UIElements值,然后编写一个辅助方法makeViewTestable ,该方法包含对UITestablePage协议方法的调用,该方法会将视图元素注册为页面元素。

我们将在SearchResultsTests使用新创建的页面:

在这里,您可以看到所有前面提到的assert方法的用法,以及页面本身的用法。 基本上,此测试通过执行简单的搜索并单击标签,然后断言返回的值等于预期值(由我们的存根机制设置),来复制用户在此屏幕上可以执行的操作,最后清除搜索栏中的文字,并断言之后没有任何结果。

这是一个简单但说明性的示例,它显示了基于POM的UI测试系统背后的全部原理。
页面和测试可能会或多或少复杂,这取决于用例和所需的粒度。
因为大多数应用程序逻辑都包含在单元测试中,所以我们通常将UI测试限制为“快乐流”,因此,如果我们破坏了它们,则CI系统将自动通知我们。

结论

POM使我们能够抽象出XCUITest带来的大多数复杂性。 通过在我们的UI测试中实现它,我们可以拥有一个简单易维护的系统,该系统易于使用,并可以扩展为将来可能需要的任何功能。