NS for iOS Devs —可测试性

在软件开发社区中,测试通常是一个大讨论。 我们听到很多人说“如果您不编写测试,您就是一个糟糕的开发人员”或“如果您不知道如何编写测试,那么您做错了所有事情”或“测试是好的,但它们耗时”。 但是,我更喜欢说

“如果您不编写测试,那就可以了。 只需尝试编写它们,您将最终了解其好处,并且您将学习和思考有关在编码之前所做的体系结构决策的更多信息。

我与很多人交谈,在聚会和会议上观看了很多关于各种测试的演讲。 一段时间后,我意识到相信通常的想法:“在iOS中,如果您有质量检查人员,则无需编写测试。” 这仍然不时地使我退缩。 但是,无论我们是否认为相同或不喜欢编写测试,我们都应该承认其好处。 相信我,当我看到及时带来的好处时,我改变了主意并开始编写自动化测试,即使我们有质量检查人员。

首先,我们应该考虑没有测试的工作流程,并确定(未来)问题。 让我们考虑一下只有一个质量检查人员而没有自动化测试的情况。 我们需要在编码之前和编码期间考虑所有情况。 仅仅因为人性,我们往往会犯错误。 这就是为什么手动测试有问题的原因。 会有不想要的行为。 我听说您,您有一个出色而勤奋的质量检查人员来测试构建。 但是小错误往往会被遗漏,并且可以通过自动测试轻松识别。

另一方面,当我们进行自动化测试时,我们倾向于减少错误。 我们创建测试用例并修复代码以使测试通过。 仅思考和编写这些案例会带来更多好处。 它们成为意图的记录。 它为项目中的新开发人员提供了足够的信息。 因此,在入职过程中,测试会拉平学习曲线。

编写更少的代码是大多数开发人员想要的事情之一。 通常,这是反对测试的有力论据。 但是,如果没有测试,我们最终将花费更多的时间来发现错误并进行修复。 没有一种简单的方法可以衡量其影响程度。 但是测试编写的项目往往更稳定,更可靠。 即使添加了新功能,我们也可以放心。 因为如果我们的更改影响了代码的其他部分,则测试失败将通知我们。

通过测试可以更好地做出体系结构决策。 我们将在以后的文章中讨论架构,但是我只想说,在编写测试时,我们最终会思考很多架构方法。 我们开始考虑单一责任和依赖倒置原则,以建立更好的架构。 有时我们需要对服务进行模拟或存根(通常在Swift中使用protocol来完成),这有助于理解面向协议的编程。 因此,我们在不知不觉中开始研究和学习更多的架构方法和设计模式。

最后,测试使代码审查变得容易。 如果我们已经进行了很多测试,我们将更有信心更改不会影响代码的其他部分。 而且,如果我们对拉(或合并)请求的内容不是很熟悉,那么通过测试,我们可以轻松理解该内容。 我喜欢Apple工程师在WWDC17的一次会议上说的话:“测试代码的代码审查,而不是带有测试代码的代码审查。”

好的,我们总结一下,直到这里。

  • 手动测试存在错误,可能会导致不良行为
  • 自动化测试功能强大,可为代码提供自我文档
  • 自动化测试首先看起来很耗时,但从长远来看,它们可以节省更多时间
  • 自动化测试可以帮助我们理解架构方法,甚至可以在不注意的情况下教很多东西。
  • 自动化的测试使代码审查更加容易。

关于iOS应用编写测试的大量在线教程非常出色(下面是链接)。 在这里,我们将重点介绍要点和一些测试技巧。

“设计代码以实现可测试性” — John Sundell

正如约翰所说,我们应该问自己一个问题: “是什么使代码易于测试?” 。 当我们问这个问题时,有两件事要想到:

  1. 我们不应该过度使用单例。

单例很棒,Apple也在重要的地方使用它们,例如UIScreenUIApplication 。 因为我们在运行时只有一个屏幕和一个应用程序,所以这很有意义。 但是使对象成为全局对象并非总是必要的。 我们应该将对象的状态保持在本地,而不是让所有人更改状态。 因此,当我们创建一个单例时,我们应该三思。

2.我们应该使用协议和参数化。

而不是出于模拟目的而进行子类化,我们应该首选协议(组合而不是继承)。 协议提供了更强大的解决方案。 当我们子类化为测试创建模拟时,如果我们忘记override函数,则Xcode不会给出任何警告。 这是有风险的,并且某些类不能被子类化(例如UIApplication )。 如果使用协议,则我们将抽象实现并创建适当的模拟。 同样,当我们忘记遵守协议而实现一个功能时,Xcode也会显示错误。 让我们看一个例子。

假设我们要创建一个FileOpener ,其唯一目的是在URL正确的情况下打开文件。 我们将使用UIApplication.canOpenURL(url:)方法。 我们将为此编写测试。

这应该在应用程序中起作用。 但是在UITests中,这将打开另一个应用程序,并且我们的测试将被阻止。 当我们开始思考John的问题( “什么使代码易于测试?” )时,我们意识到我们应该首先对函数进行参数化。 UIApplication是类中的依赖项。 因此,最好注入它。

如我们在初始化程序中看到的,我们在默认参数中使用了UIApplication.shared 。 这使得初始化DocumentOpener非常容易。 但是我们仍然有一个问题。 我们无法模拟UIApplication因为它是一个单例。 现在,我们可以在Swift中获得协议的强大功能。 让我们实现一个称为URLOpening的新协议,并使UIApplication遵循它。

现在,让我们调整FileOpener并将协议用作初始化程序中的参数。

由于有了Swift扩展,我们不需要在UIApplication扩展中实现URLOpening协议,如上所述。 因为我们遵循的是UIApplication已经拥有的相同方法签名。 现在,我们对实现进行了抽象,我们可以创建一个新的模拟类并仅遵循URLOpening协议。 因此,我们将能够在测试FileOpener使用FileOpener

分开逻辑和效果,并为API创建明确的界限

我们应该创建框架和库来分离逻辑。 使用关注点分离,我们可以提取业务逻辑和算法。 他们可以进行自己的测试。 而且,每当我们需要更改某些内容时,我们都会知道,如果我们设计合理的API边界,所做的更改将不会影响业务逻辑。 Swift具有强大的访问控制。 我们不应该向那些框架之外提供更多的信息。

使用纯函数

我们应该利用功能样式并减少功能的影响。 给定相同的输入时,一个函数应始终返回相同的输出。 另外,它不应该有任何副作用。 (在函数式编程中,这些函数称为“纯函数”)。 纯函数是可预测的,并且易于测试。

优化应用启动以进行测试

在运行测试时,我们看到了模拟器,但要等待几秒钟。 这是因为应用程序正在加载。 测试不会在application(_ application:didFinishLaunchingWithOptions:)返回之前开始。 我们通常在此方法内进行很多设置,例如分析和崩溃报告设置。 但是我们通常在测试期间不需要它们。 因此,在启动应用进行测试时,我们应该避免不必要的工作。 我们可以设置一个自定义方案环境变量,并在AppDelegate使用它。

避免过多的嘲笑

在测试时模拟效果很好。 但这可能会导致很多实现细节。 嘲讽可预测。 每当我们需要真正好的可预测性时,我们都可以对其进行定义。 但是即使在这种情况下,它们也应该尽可能简单,并且应该是内联的,而不是全局定义的。

在测试中使用正确的期望值,避免模棱两可的测试

我们应该在单元测试中使用更快的基于回调的期望:

  • XCTestExpectation
  • XCTNSNotificationExpectation
  • XCTKVOExpectation

在Xcode 10中使用并行测试功能

如果您有很多测试,请使其并行运行。 Xcode对它们进行了很好的优化。 它减少了测试运行时间。 在并行运行时,请注意测试类的执行时间。 如果一个测试课程花费大量时间,而其他测试课程则不需要,请尝试将该课程分成多个课程,或者找出花这么长时间的原因。 这将加速运行测试。

用测试覆盖应用程序代码是一种确保应用程序正常运行的好方法。 我们不仅应设定测试覆盖率的目标,而且还应像对待应用程序一样谨慎对待测试。 即使代码没有交付,测试代码的质量也非常重要。 应用代码中的编码原则也应适用于测试代码。 最后,测试代码应该支持我们应用程序的发展,并且应该一起成长。 因此,我们应该通过代码覆盖来关注它们的增长。

您如何看待提示? 您有一些有关测试的提示吗? 您遵循哪些策略来编写或改进测试? 让我知道您对Twitter @candosten的想法,评论或反馈,或下面的评论。


NS for iOS Devs Series的所有帖子

  • 应用生命周期
  • 查看生命周期
  • 并发
  • 可测性

进一步:

可测试性工程— WWDC17

编写具有可测试性的Swift代码— John Sundell

测试技巧和窍门— WWDC18

在Swift中嘲笑-Swift由Sundell提供

面向协议的编程— WWDC15

iOS单元测试和UI测试教程— Ray Wenderlich

适用于iOS的测试驱动开发教程— Ray Wenderlich

Xcode的秘密性能测试-独立堆栈


想了解更多? 阅读本系列的其他文章:

NS for iOS Devs —应用程序生命周期

每个iOS开发人员都需要了解iOS应用程序的可能状态和生命周期。

medium.com

NS for iOS Devs —查看生命周期

真正了解视图的创建,加载,显示或销毁时间有助于我们深刻理解我们的方法……

medium.com

NS for iOS Devs —并发

我们面临同时处理多个操作的问题。 我们想通过使用…来缩短用户的等待时间。

medium.com