在 iOS 中正确地进行单元测试

为什么每次发布后我的应用程序都会出现这么多错误?

为什么我的 QA 团队经常报告重复性问题和崩溃?

遏制此类问题 的最佳解决方案是单元测试。 在这篇博文中,我们将了解如何将单元测试有效地整合到我们的代码库中,从而减少重复测试工作和错误频率。

有 3 种类型的开发人员级别测试:

  1. 单元测试(我们今天将重点关注这一点)
  2. 集成测试
  3. 界面测试

单元测试基本上是将更大的复杂逻辑分解为更小的可测试逻辑。

为现有代码库编写单元测试可能是一个巨大的挑战。这就是为什么我们应该随着代码继续开发单元测试用例的原因。所以理想的开发工作流程应该是这样的:

  1. 制作 BRD 和后端工作流程(您的 UML)。
  2. 制作设计文档。
  3. 根据每个功能/模块的业务逻辑编写测试用例。
  4. 通过将较大的逻辑分解为较小的部分,开始为每个功能编写单元测试用例。
  5. 开始构建和即兴构建单元测试用例的逻辑基础。

由于我们不是生活在一个理想的世界中,我们将不得不为现有的代码库编写测试用例。在开始一个新项目时,可以使用 测试驱动开发、面向协议编程和依赖注入等技术轻松 进行单元测试,而在对现有代码库/项目进行单元测试时,我们将不得不结合这些技术,然后进行大量代码重构(我的意思是很多代码重构)。

XCTest 库

XCTest 库提供了一个通用框架,用于在 Swift 中为 Swift 包和应用程序编写单元测试。我们可以在创建新项目时包含单元测试用例,也可以稍后将单元测试目标添加到我们的项目中。在您的测试类中提及您的可测试目标(要测试的应用程序)以访问其类。一个单元测试用例生命周期有2个主要方法​​​​​​​

  1. setUp()
  2. tearDown()

这些方法分别在每个测试用例执行之前和之后自动调用。我们可以在这些方法中执行任何资源分配-解除分配。

注意:测试用例应该完全相互独立,因为它们是异步执行的。

什么?为什么?如何?一个单元测试用例

什么 是有效的测试用例?一个既可以通过也可以失败的。大多数时候,我们测试应用程序的核心业务逻辑,它由以下几部分组成:

  • 数据解析和数据操作:来自 API、文件、缓存、数据库、本地存储、用户默认值、共享对象、全局变量、常量等的数据。
  • 实用程序/支持类:日期格式化程序、字符串操作逻辑、数据验证检查、nil 检查等逻辑。
  • 类中的逻辑,如扩展、数据管理器、数据模型、视图模型(特定于特定模块)。
  • 方法包含 if-else 逻辑、switch、map-reduce-filter 逻辑等。

我们需要识别和隔离每个可测试的部分,使其独立于其他代码。例如,一种方法应该只执行一项任务,例如检查输入文本中的有效数字。

如何? 现在我们知道必须测试哪些部件,让我们看看如何测试它们。

1. 模拟是使用虚拟数据或模拟数据测试您的代码逻辑。使用模拟数据编写单元测试用例提供了接近真实的测试模拟,使其成为单元测试的理想方式。列出需要模拟的代码片段:

  • 共享实例、局部和全局变量、数据模型
  • 文件和其他本地资源
  • Core Data 和其他数据库,如 SQLite
  • Apple API,例如 CNContact、钥匙串商店实例、位置等
  • 网络通话
  • 缓存数据

模拟单元测试的数据有时会很痛苦,尤其是网络调用。我们可能不得不在模拟数据方面投入更多的编码工作,而不是代码本身的实际开发。有时开发人员必须模拟整个类。列出两种抑制模拟问题的方法:

  • 依赖注入:它有助于独特地初始化一个类或对象,以便它可以很容易地被模拟。例如,使用数据源作为强制参数初始化控制器类,或者使用作为参数传递的共享会话初始化网络调用,从而使模拟更容易。
  • POP(面向协议的编程):它有助于以松散耦合的扩展和协议的形式抽象类的代码。扩展方法不能在我们的 XCTest 类中直接测试,我们只需要通过扩展协议暴露扩展特定的逻辑。一个用于测试 ContactsManager 类的部分模拟 CNContact 的示例

有些情况可以通过部分模拟来测试,而有些情况需要完全模拟正在测试的对象。如果将这种方法应用于现有代码库,则需要大量代码重构。

2. 存根 是使用自定义框架/库来存根网络响应而不实际对服务器进行任何网络调用。在这里,我们可以完全控制要测试的响应,而无需任何 Internet 或服务器依赖。在编写单元测试用例时,存根需要更少的编码工作。一些最流行的存根库是  用于 XCTest(单元)测试的OHHTTPStubs、  Mockingjay、  Hippolyte 。

可以说,我们将能够使用存根模拟大多数快乐的测试场景。如果我们从实际服务器收到的响应有任何添加或更改,我们将不得不更新我们的存根。

3.  TDD(测试驱动开发) 将显着减少开发人员的模拟和存根工作,或者更确切地说,使模拟更容易,因此我们根本不需要存根数据进行单元测试。顾名思义,测试驱动开发会影响开发方法,使生成的代码更加独特、独立和可测试。

开发人员通过将较大的逻辑分解为较小的部分来开始为每个功能编写单元测试用例,然后继续开发和即兴创作过程中确定的单元测试用例的逻辑基础。TDD 专注于被测试的逻辑部分的症结,因此隔离了代码的各个可测试部分。我将尝试在另一篇文章中通过一个示例来介绍 TDD。

注意:当我们想要对一段代码进行单元测试时,我们必须检查“

为什么”。问问自己“是否应该在单元测试下测试这段代码”,或者在这种情况下是否有其他测试措施可以更好地自动化测试。我们无法测试只返回特定字符串(如字体、颜色、键或常量)的方法。此外,我们应该确保我们不会向一个类添加太多代码以使其可测试,然后编写单元测试用例来测试为测试原始类而添加的额外代码。我们不想陷入那个螺旋循环。始终,找到一种方法来编写尽可能少的模拟和数据操作的单元测试用例。专注于逻辑的症结。

面临的挑战

  • 为现有的遗留代码 VS 新代码编写测试用例。
  • 使代码可测试,然后进行代码重构(大量代码重构)。
  • 无法直接访问私有变量以进行模拟 – 使用 getter setter 方法。
  • 它不能直接测试私有方法。

优点

  • 充当代码文档。我们将在单元测试用例中很好地分离大部分业务逻辑测试用例。
  • 显着降低错误频率。是的,确实如此,从个人经验来看。
  • 它使我们编写更好的代码。
  • 在重构现有代码时很有用。当我们重构或进行代码升级时,破坏它的机会很高。测试用例会立即指出错误的方向,我们可以在错误进入 QA 之前修复它。
  • 也可以(在一定程度上)测试应用程序行为。
  • 降低了圈复杂度,使代码更简单、更健壮。理想的圈复杂度应该在 4-7 之间(良好到中等范围)。

如果您想了解有关圈复杂度的更多信息,请观看。它很好地解释了这个令人费解的术语。 

缺点

  • 代码必须重新编写和重构很多以使其可测试。

代码覆盖率

理想的代码覆盖率应该是 80% – 100%。Xcode 具有用于单元测试的规定检查类明智的代码覆盖率。

开发管道

下图描绘了当我们将单元测试集成到代码库中时典型管道的外观。我们可以在提交代码之前手动运行单元测试用例,也可以使用脚本在 CICD 管道中自动运行单元测试用例,或者两者都做(推荐)。

这是一篇关于单元测试的信息性文章。下一篇文章将更加面向编码,涵盖依赖注入、存根和 POP。

approaching-unit-testing-in-ios-correctly-lmr3u13