/

【译】iOS 单元测试和 UI 测试入门教程

原文链接:iOS Unit Testing and UI Testing Tutorial - Ray Wenderlich

编写测试并不迷人 (glamorous),但是既然测试能让你闪闪发光 (sparkling) 的应用程序变成 (from turning into) 一堆乱七八糟的垃圾,那么说明测试是必要的。如果你正在阅读 iOS 单元测试和 UI 测试入门教程,那么你已经知道 应该 为代码和 UI 编写测试,但是你不知道如何在 Xcode 中进行测试。

也行你已经有一个 “可以运行” 的 app 但是没有为它编写任何测试,并且当你想拓展 app 时,你能够测试任何的改变。也许你已经编写了一些测试,但是不确定他们是否是 正确 的测试。或者你现在正在开发你的 app,想在你离开的时候进行测试。

这篇入门教程将告诉你如何使用 Xcode 的测试导航栏去测试一个 app 模块和异步方法 (asynchronous methods),如何模拟交互 fake interactions 或者系统对象通过 subs and mocks,如何测试 UI 和性能 performance,和如何使用代码覆盖率工具 code coverage tool。在此过程中 Along the way,您将会学到一些用于测试忍者 testing ninjas 的词汇 vocabulary,在本教程的最后,你将使用依赖注入对您的系统进行测试 you’ll be injecting dependencies into your System Under Test (SUT) with aplomb!。

测试,测试

测试什么

在编写任何测试之前,一个开始时非常重要的基础是:你需要测试什么?如果你的目标是拓展一个已经存在的 app,你首先应该为 任何你计划改变的组件 编写测试。

更普遍的讲,测试应该覆盖:

  • 核心功能 Core functionality:模型、类和方法,和与他们相互作用的控制器
  • 最常见的 UI 工作流
  • 边界条件 Boundary conditions
  • Bug 修复

第一件事 FIRST:测试的最佳实践

FIRST 首字母缩写词描述了有效单元测试的一套简明标准。这些条件是:

  • Fast:运行测试应该快速,这样人们就不会介意运行它们了。
  • Independent/Isolated:测试不应该对彼此进行设置或拆解 setup or teardown。
  • Repeatable:每次运行测试时,都应该得到相同的结果。外部数据提供者和并发问题可能导致间歇性故障。
  • Self-validating:测试应完全自动化;输出应该是“通过”或“失败”,而不是程序员对日志文件的解释。
  • Timely:理想情况下,测试应该在编写生产代码之前编写。

遵循 FIRST 原则,你的测试将保持清晰和有帮助,而不是为你的 app 设置障碍。

开始

下载,解压,打开,检查 初始项目 BullsEye 和 HalfTunes。

BullsEye 是一个基于 iOS Apprentice 的示例 app。已经将游戏逻辑提取到一个 BullsEyeGame 类中,并添加了另一种游戏风格。

在右下角,有一个分段控制,让用户选择游戏风格:Slide,移动滑块尽可能接近目标值,或者 Type,来猜测滑块的位置。控件的操作还将用户的游戏样式选择存储为用户默认值。

HalfTunes 是 NSURLSession Tutorial 中的示例 app,更新至 Swift 3。 用户可以通过 iTunes API 查询歌曲,然后下载并播放歌曲片段。

让我们开始测试!

Xcode 中的单元测试

创建一个 Unit Test Target

Xcode Test Navigator 提供了使用测试的最简单的方法;你将使用它创建 test targets 和在 app 运行测试。

打开 BullsEye 项目然后敲击 Command + 6 打开 test navigator。

点击右下角的 + 按钮,然后从菜单中选择 New Unit Test Target…

接受默认的名字 BullsEyeTests。当 test navigator 中出现 test bundle,点击打开。如果 BullsEyeTests 没有自动出现,通过单击其他 navigators 中的一个来解决问题 trouble-shoot by,然后返回到 test navigator。

模板导入了 XCTest 并定义了 XCTestCase 的一个 BullsEyeTests 子类,还有 setup() tearDown() 和 testExample()。

有三种方法运行 test class:

  1. Product\Test or Command-U. 将运行所有的 test classes
  2. 在 test navigator 中点击 arrow 按钮
  3. 在边沿点击菱形按钮

您还可以通过单击菱形按钮来运行单个测试方法,无论是在 test navigator 中还是在 gutter 中。

当所有的 tests 成功后,菱形按钮就会变绿并显示出检查的痕迹。单击 testPerformanceExample() 末尾的灰色菱形,打开性能结果:

你不需要 testPerformanceExample(),所以删除它。

使用 XCTAssert 测试 Models

首先,将使用 XCTAssert 去测试 BullsEye model 中的一个核心方法:BullsEyeGame 对象是否正确计算一个回合的分数。

在 BullsEyeTests.swift 添加

@testable import BullsEye

这使得 unit tests 可以访问 BullsEye 中的类和方法。

在 BullsEyeTests 顶部添加属性

var gameUnderTest: BullsEyeGame!

在 setup() 创建一个新 BullsEyeGame 对象,在 super 之后

gameUnderTest = BullsEyeGame()
gameUnderTest.startNewGame()

这将在类级别创建一个 SUT (System Under Test) object,因此这个 test class 中的所有测试都可以访问 SUT 对象的属性和方法。

在这里你也可以调用 startNewGame,它将创建一个 targetValue。许多测试将使用 targetValue,来测试游戏是否正确地计算了分数。

不要忘记释放你的 SUT object 在 tearDown(),在你调用 super 之前

gameUnderTest = nil

Note: It’s good practice to create the SUT in setup() and release it in tearDown(), to ensure every test starts with a clean slate. For more discussion, check out Jon Reid’s post on the subject.

现在你已经准别写你的第一个 test!

用一些代码替换 testExample()

// XCTAssert to test model
func testScoreIsComputed() {
// 1. given
let guess = gameUnderTest.targetValue + 5

// 2. when
_ = gameUnderTest.check(guess: guess)

// 3. then
XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}

一个 test 方法总是以 test 开头,接下来是对它测试的描述。

将测试格式化为 given when then 是很好的做法:

  1. 在 given 部分,设置需要的值:在此例你创建一个 guess 值,所以你可以指定它与 targetValue 有多大的不同。
  2. 在 when 部分,执行正在测试的代码:调用 gameUnderTest.check(_:)
  3. 在 then 部分,断言你所预期的结果(在本例,gameUnderTest.scoreRound is 100 – 5)如果测试失败,则会打印一条消息。

Note: To see a full list of XCTestAssertions, Command-click XCTAssertEqual in the code to open XCTestAssertions.h, or go to Apple’s Assertions Listed by Category.

Note: The Given-When-Then structure of a test originated with Behavior Driven Development (BDD) as a client-friendly, low-jargon nomenclature. Alternative naming systems are Arrange-Act-Assert and Assemble-Activate-Assert.

Debugging a Test

在 BullsEyeGame 故意留下了 bug,所以现在你要练习找到它。为了找到 bug 将 testScoreIsComputed 改名为 testScoreIsComputedWhenGuessGTTarget。在这个 test 中,在 given 部分从 targetValue 减去 5

func testScoreIsComputedWhenGuessLTTarget() {
// 1. given
let guess = gameUnderTest.targetValue - 5

// 2. when
_ = gameUnderTest.check(guess: guess)

// 3. then
XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}

guess 与 targetValue 之间仍然是 5,所以分数应该还是 95。在 breakpoint navigator 添加 Test Failure Breakpoint,这将在断言失败时停止测试运行。

运行你的 test:当测试失败时它将停在 XCTAssertEqual 这行。检查 gameUnderTest 和 guess 在 debug console

guess 是 trgetValue - 5 但是 scoreRound is 105 不是 95!

进一步研究,使用正常的调试过程:在 when 声明处和在 BullsEyeGame.swift 设置断点,在 check(_:) 中产生了差异。然后再次运行测试,并且 step-over let difference 检查 difference 的差异值:

问题是 difference 是负的,所以得分是 100 – (-5),使用 difference 绝对值来修复这里。

移除两个断点然后再次运行测试以确认它现在成功了。

待续