测试UITableViews数据源(第1部分)

在移动开发中,测试应用程序是一项非常重要的任务,特别是当您的产品需要经常发货并且您不是一个人的团队时。

但是,即使自第一天起就不应该将持续集成作为目标,但我们都知道,在现实世界中,我们大多数人都会跳入已经由其他人启动并且具有许多未经测试的代码的项目。

考虑到这一点,这将是我的第一篇有关代码测试及其有关在Swift中测试UITableView组件的文章。 更具体地说,有关如何测试UITableViewDataSource。

我选择该主题的主要原因是我花了大部分工作时间在Swift中进行开发,并且UITableViews是所有iOS应用程序中使用的最基本的组件之一。

代码结构

有关此项目的代码可在此处找到,并且由一个包含UITableView的单屏应用程序组成。 由于UITableViews是非常复杂的组件,并且我们可以使用许多不同的方式,因此本示例重点介绍如何测试所需的UITableViewDataSource方法,这些方法是:

如果下载并运行代码,您将看到一个空行的屏幕,该屏幕可滚动,但没有太多显示。 我曾经认为我需要查看自己所做的所有事情才能正确测试它。

一段时间后,我意识到可以分离逻辑和UI并分别对其进行测试。 这听起来像是显而易见的推理,但有时实践并不像理论那么容易。

TL; DR如果您对测试部分比对项目本身更感兴趣,请参考“ 测试”部分

AppDelegate.swift

这是我们应用程序的起点。 它有很多样板,通常我们不需要触摸它就可以开始开发任何东西。 但是,在这种情况下,我想将UIViewController与情节提要隔离,因此我插入了一个函数来手动设置RootViewController

和功能:

该函数的作用是创建一个TodoListViewController实例并将其设置为我们的窗口rootViewController 。 通过这样做,我们将应用程序直接加载到我们要测试的UIViewController中

作为本文的补充,我们还将看到如何使用UI Test来测试是否正确加载了初始视图控制器。

TodoListAdapter.swift

由于我们要正确测试UITableViewDataSource ,因此最好将其与UIViewController分离。 为此,我们创建一个单独的类来实现数据源协议。 该适配器类也可以用于实现UITableView的委托,但这超出了本文的范围。

现在,让我们集中讨论如何实现数据源。 由于我们的适配器的主要目标是实现UITableViewDataSource协议,因此我们需要将该接口添加到适配器的声明中。 我们还需要添加一个NSObject 接口,因为UITableViewDataSource 需要符合NSObjectProtocol 太:

常数

为了能够利用iOS内存优化的优势,我们应该为UITableViewCells分配一个标识符,该标识符将使它们在需要时出队并再次使用,而不是每次都进行新分配。

由于我们的适配器将负责管理UITableView内容,因此让我们在其中添加一个常量,使我们可以在具有相同标识符的代码库之间保持一致。

变数

由于我们想将数据与UIViewController分离,所以我们创建一个变量以将我们的数据存储在适配器中,并创建一个适当的初始化器来接收该数据:

这样,每次创建适配器时,我们都可以为其分配用于呈现UITableView的数据 。 另外,这对于测试非常有用,因为我们可以根据需要在适配器内部直接模拟数据。

为简单起见,我们的数据将为Strings数组。 在以后的文章中,我将更好地解释如何在适配器内处理不同的数据类型,但是现在使用Strings就足够了。

UITableViewDataSource

从根本上说,在测试UITableViewDataSource时,我们希望能够检查数据是否正确加载。 就像我之前说过的那样,有两种方法需要实现:

  • tableView(UITableView,numberOfRowsInSection:Int)

此方法负责定义UITableView具有的数据的行数。 由于我们要在表视图中显示所有数据,因此我们将使其返回数据数组计数。

  • tableView(UITableView,cellForRowAt:IndexPath)

一旦知道表将要包含多少行, 数据源就需要为每个表分配一个UITableViewCell 。 该视图将在每行上显示,并且可以完全自定义。 出于本文的目的,我们将使用标准的UITableViewCell

let语句上,我们从tableView请求一个单元格。 只要我们在UITableView中具有在UITableViewCell子类或自定义nib中注册的cellIdentifier ,此方法就确保返回非null的单元格。

要注册我们的cellIdentifier,我们需要对UITableView的引用,并且出于适配器的目的,在其上添加一个不是有用的。 就是说,我们将在UIViewController中注册cellIdentifier ,它具有UITableView的所有权,因此可以对其进行访问。

TodoListViewController.swift和TodoListViewController.xib

这是我们的UIViewControllerxib夫妇。 XIB文件由一个UIView组成,其中UITableView固定了页边距。 由于我们正在测试单个控制器,因此添加其他组件(如按钮或导航栏)毫无意义。 在本文的下一部分中,我们将进行一些更改以将UITableView集成到更实际的上下文中。

网点

由于我们使用的是xib文件,因此需要使用and outletUIViewController内链接UITableView 。 这样,我们将能够从控制器访问UI组件:

变数

关于变量,我们的UIViewController仅需要一个TodoListAdapter实例:

此变量封装了管理UITableView所需实现的所有协议。 许多教程和示例都在UIViewController本身中实现了UITableViewDataSource 。 但是,这种方法很难测试这些组件。

辅助功能

最后,我们创建一个可访问性结构。 这个组件将负责定义我们将在UIViewController中声明的所有可访问性标签

在我们的测试部分的后面,我将更多地讨论为什么在代码中定义它很重要,以及该结构如何帮助维护我们的代码库。

初始化,覆盖和设置

一旦定义了UIViewController的所有出口,变量,常量和常规对象,我们就可以移至其实现。 在此示例中,我们将重写UIViewController中的单个方法viewDidLoad

只要这些设置不需要布局信息,就应该使用此方法来设置所有组件。 在viewDidLoad上 ,由于尚未调用自动布局功能,因此我们仍然没有根据设备调整视图的大小,但是我们已经安装了所有插座 ,并在创建视图时将信息传递给了UIViewController ( (通过xib或在以前的控制器中手动)。

我个人喜欢保持我的viewDidLoad方法干净。 我没有在其中添加一堆逻辑,而是尝试在fileprivate方法中分离不同的设置。 当我们需要快速了解UIViewController的加载方式时,这非常有用,因为设置方法的名称应易于说明。 当我们将所有内容组合在一起以混合不同的组件设置时,这非常容易,这使得很难阅读代码并最终对其进行重构。

在这种情况下,我有两种设置方法,一种用于UITableView ,另一种用于accessibility

在第一个中,我们做两件事:我们在tableView中注册一个UITableViewCell类,并将其dataSource设置到我们的适配器 。 还记得我们在TodoListAdapter中定义的cellIdentifier吗? 好吧,我们在这里使用它将其绑定到正确的单元格类。 由于我们正在从适配器中加载标识符,因此我们不必担心以后在适配器中对其进行更改,因为它不会破坏我们的tableView

另外,由于其协议兼容性,我们可以将TodoListAdapter设置为tableView数据源。 由于我们的适配器实现了UITableViewDataSource接口,因此我们可以将其分配给UITableView,而不必担心转换。 在Swift中使用协议的优点之一。

第二种方法是设置tableView的可访问性标签。 有了这个可访问性标签,我们将可以在UI测试中访问tableView 。 即使UI测试不在本文的讨论范围之内,我也将提供一个示例,说明如何在应用程序运行时执行它们以验证我们的组件是否正确。

测试中

最后是有趣的部分,有了我们的代码,我们就可以开始测试。 如果您习惯于测试驱动开发(TDD),您将阅读此书,并认为一切都错了,因为应该先编写测试,然后编写代码,然后进行重构,然后重复执行。 我完全同意你的看法。 但是,本文的目的不是要在Swift中进行开发时如何进行TDD ,而是要如何测试特定组件。 一旦了解了操作方法,就可以根据需要进行多次TDD

TodoListAdapter

让我们从我们的TodoListAdapterTests类开始。 顾名思义,是负责测试我们的适配器的一组测试。 这是此示例的主要测试类,因为它将测试我们的UITableViewDataSource 。 让我们分部分分析它。 首先,我们有变量:

我们有一个TodoListAdapter变量,这是我们要测试的变量,还有一个UITableView实例,我们需要模拟 UITableView的行为。

在我们的setUp方法中,我们将实例化UITableView并为cellIdentifier注册一个UITableViewCell类。 此操作对所有测试都是通用的,因此可以将其放入此方法中。

我还定义了一个私有方法来使用不同的数据数组初始化适配器。 此方法在fileprivate扩展名下

我们可以为每个测试使用相同的数据初始化适配器,但这会带来风险。 为什么? 让我们以第一个测试为例:

此测试使用数据数组初始化适配器,并断言numberOfRowsInSection返回正确的行数。 tableView的行数自然应该与我们的数据数组数量匹配,测试是否在请求时适配器将为tableView提供正确的行数。

现在,我们假定TodoListAdapter内部的numberOfRowsInSection实现如下:

如果由于某种原因我们为适配器函数定义了一个固定的返回值,并且我们在一个大小与我们的固定值相等的数组上(巧合地)对其进行了测试,那么即使我们的数据源实现不正确,我们的测试也会通过。 现在,如果我们添加另一个具有不同数据数组的测试,那么其中一个测试将失败,并且我们将知道有问题。

使用不同的数据集进行测试始终很重要,因此我们可以捕获此类错误。 当然,我们不能测试所有情况,但是我们需要测试足够的情况,以确保我们的代码稳定可靠,因此我们可以快速发现错误和错误,并拥有更稳定的代码库。 测试的数量取决于所涉及的逻辑,因为我们只是加载数据数组,所以两种不同的情况足以确保数据正确。

最后但并非最不重要的一点是,我们将测试单元中是否正确加载了数据。 为了避免冗长的功能,我还创建了一个名为assertText的帮助器函数(在fileprivate扩展内部定义):

与之前的测试一样,我们使用数据数组初始化适配器。 之后,我们检索每个tableView单元格,以将其文本与数据数组进行比较。 在此示例中,我们手动测试了每一行,但是我们可以遍历数组并针对每种情况调用helper方法以避免代码重复,并测试大数据集,以防数据集的大小可能以某种方式破坏数据源。

TodoListViewController

测试适配器后,我们可以继续测试UIViewController 。 由于我们的适配器逻辑已经过测试,因此我们现在需要测试以下内容:

  1. 我们的UITableView已连接到我们的UIViewController
  2. 我们的可访问性是正确的

在我们的测试类中,我们创建一个可访问性副本,以便能够访问我们要在测试中使用的对象的已定义标签和实例:

最后,我们使用数据数组,适配器和UIViewController的适当初始化来定义setUp方法:

重要的是要强调有关此设置的几点。 第一个是我们使用在TodoListViewController内部创建的便捷初始化 。 第二个是我们断言控制器的视图不是nil 。 在以后的文章中,我将更多地讨论如何测试UIViewControllers,但现在您需要知道需要此断言,以便可以加载视图并可以访问所有出口

加载视图后,您可以测试资产是否已正确加载,在本例中,唯一的出口就是tableView 。 我们还测试了我们的dataSource是否正确设置:

这很重要,因为它可以帮助我们捕获在重构期间或对视图进行任何类型的更改时,由于从xib文件中意外删除的出口而导致的错误。 更容易进行测试以使我们知道某个组件已断开连接,从而花费数小时试图找出为什么tableView的数据未加载的原因,即使“一切就绪”也是如此。

我们还将测试可访问性标签是否设置正确。 通过这样做,我们可以提前捕获UI测试失败:

额外

还记得我们的rootViewController设置,以及我们如何关心定义和测试可访问性是否正确? 嗯, accessibilityLabel在进行UI测试时非常有用,因为它们可以帮助我们找到需要测试的特定组件。 在此示例中,我创建了一个UI测试来断言应用程序是否以我们的TodoListViewController加载为起点,毕竟,如果对用户不可见的控制器逻辑进行了完美的测试,那么它就没用了吗?

在以后的文章中,我将更多地讨论UI测试,但是到目前为止,足以说明我们正在测试的是在应用程序启动之后, 是否 存在我们在TodoListViewController中声明的带有可访问性标签的表。

结论

拥有强大的测试基础是在任何平台上开发可扩展应用程序的基础,并且具有从较小组件到较复杂组件进行测试的基础。 Swift协议为我们提供了测试不同组件及其集成的惊人可能性。

即使这里提出的示例非常简单,对于那些想了解如何测试Swift基本组件的人来说,它也是一个起点。 在以后的文章中,我将更深入地解释如何使用更复杂的视图层次结构测试另一种数据源方法。