Skip to content

Latest commit

 

History

History
1371 lines (1337 loc) · 60.5 KB

Unit Testing on macOS - Part 2:2.md

File metadata and controls

1371 lines (1337 loc) · 60.5 KB

单元测试:2/2部分

Unit-Testing-macOS

在本教程的 第一部分 ,你已经学习了如何使用TDD来测试新添加的代码,及添加单元测试到已存在的代码中。在这一部分,你将学习如何测试UI,如何测试网络代码,以及一些帮助你进行测试的Xcode工具。

如果你还未完成第一部分,或是想要一个新的开始,可以从 这里 下载第一部分完整的项目。该项目使用Swift 3,最低Xcode 8 beta 6以上的版本。在Xcode中打开它并按下 Command-U 键来运行所有的测试,确认一切如你期望中一样得工作正常。

测试交互

正如你在第一部分中所看到的一样,Xcode包含了运行UITests的能力。尽管这个非常有用,单用编程的方式来测试view和view controller可以更加快速。在测试中我们会创建一个view或view controller的实例去直接测试,而不是运行app并发送模拟点击事件到交互对象上。你可以获取和设置property,调用方法 - 包括IBAction方法 - 来更加快速地测试出结果。

File Navigator 中选择 High RollerTests 组,并使用 File/New/File… 中的 macOS/Unit Test Case Class 来创建一个名为 ViewControllerTests 的类。添加全部的代码并添加下列的import到文件的顶部:

@testable import High_Roller

插入下列的代码到 ViewControllerTests 类中:

// 1
var vc: ViewController!
override func setUp() {
super.setUp()
// 2
let storyboard = NSStoryboard(name: "Main", bundle: nil)
vc = storyboard.instantiateController(withIdentifier: "ViewController") as! ViewController
// 3
_ = vc.view
}

上述的代码:

  1. 整个的类会使用一个名为 File Navigator 的property访问 ViewController 。可以将它设为非可选的,因为如果它崩溃掉的话,仍然是一个非常有用的测试结果。
  2. 这个view controller是从storyboard的 setup() 中实例化的。
  3. 为了触发view的生命周期,获取view controller的 view property。你无需去保存,因为获取它的动作就已是的创建view controller的动作正确地执行。

以这种方式实例化view controller,必须保证它有一个storyboard ID。打开 Main.storyboard ,选择 ViewController ,并在右侧打开 Identity Inspector 。将Storyboard ID设置为 ViewController

Add-Storyboard-ID-for-ViewController

第一个测试将用来确认 ViewController 正确地被创建。切回 ViewControllerTests.swift 并添加下列的测试方法:

func testViewControllerIsCreated() {
XCTAssertNotNil(vc)
}

运行测试。如果出现失败或崩溃的话,返回 Main.storyboard 并检查你的storyboard ID是否设置正确。

运行app来查看一下界面。所有的控件都是有功能的,因此点击 Roll 按钮来滚动骰子;修改设置并再次滚动,注意骰子的数量可以使用一个text field或stepper来设定,而骰子的面数则使用一个弹出菜单来设定。

BuildRun2

在测试之前,控件的操作都如同期望中一样地执行,你需要首先确认交互元素是以期望中的值开始的。

添加下列的测试到 ViewControllerTests 中:

func testControlsHaveDefaultData() {
XCTAssertEqual(vc.numberOfDiceTextField.stringValue, String(2))
XCTAssertEqual(vc.numberOfDiceStepper.integerValue, 2)
XCTAssertEqual(vc.numberOfSidesPopup.titleOfSelectedItem, String(6))
}

运行测试,以确认初始化的设置是正确的。

确认之后,下一步你就该测试,当通过交互改变参数之后发生了什么。当你在text field中修改了文本框之后,stepper的值就应当发出相应的改变,反之亦然。

如果你在使用app的过程中,通过点击向上或向下的箭头改变了stepper的值, numberOfDiceStepperChanged(_:) 方法就会被自动地调用。类似的,如果你编辑text field的话, numberOfDiceTextFieldChanged(_:) 就会被调用。在测试的时候,你必须手动地来调用这IBAction方法。

插入下列的两个测试到 ViewControllerTests 中:

func testChangingTextFieldChangesStepper() {
vc.numberOfDiceTextField.stringValue = String(4)
vc.numberOfDiceTextFieldChanged(vc.numberOfDiceTextField)
XCTAssertEqual(vc.numberOfDiceTextField.stringValue, String(4))
XCTAssertEqual(vc.numberOfDiceStepper.integerValue, 4)
}
func testChangingStepperChangesTextField() {
vc.numberOfDiceStepper.integerValue = 10
vc.numberOfDiceStepperChanged(vc.numberOfDiceStepper)
XCTAssertEqual(vc.numberOfDiceTextField.stringValue, String(10))
XCTAssertEqual(vc.numberOfDiceStepper.integerValue, 10)
}

运行测试来查看结果。你还应当测试view controller中的变量,以确认它们如同期望中的方式进行变化。

view controller含有一个 Roll 对象,它含有自己的property。添加下列的测试以确认 Roll 存在对象,并含有期望的默认property:

func testViewControllerHasRollObject() {
XCTAssertNotNil(vc.roll)
}
func testRollHasDefaultSettings() {
XCTAssertEqual(vc.roll.numberOfSides, 6)
XCTAssertEqual(vc.roll.dice.count, 2)
}

接下来,你需要确认通过界面中的元素改变一项设置后,确实可以改变 Roll 对象中的设置。添加下列的设置:

    func testChangingNumberOfDiceInTextFieldChangesRoll() {
vc.numberOfDiceTextField.stringValue = String(4)
vc.numberOfDiceTextFieldChanged(vc.numberOfDiceTextField)
XCTAssertEqual(vc.roll.dice.count, 4)
}
func testChangingNumberOfDiceInStepperChangesRoll() {
vc.numberOfDiceStepper.integerValue = 10
vc.numberOfDiceStepperChanged(vc.numberOfDiceStepper)
XCTAssertEqual(vc.roll.dice.count, 10)
}
func testChangingNumberOfSidesPopupChangesRoll() {
vc.numberOfSidesPopup.selectItem(withTitle: "20")
vc.numberOfSidesPopupChanged(vc.numberOfSidesPopup)
XCTAssertEqual(vc.roll.numberOfSides, 20)
}

这三个测试会分别操作text field,stepper和弹出菜单。在每次UI元素发生变化之后,它们就会检查 roll 这个property是否匹配于相应的值。

在assistant editor中打开 ViewController.swift ,并查看 rollButtonClicked(_:) 。它做了三件事:

  1. 确保任何正在骰子text field中编辑的值都被处理过。
  2. 告知 Roll 结构体滚动所有骰子。
  3. 展示结果。

你早已编写了测试来确认 rollAll() 的功能如同期望中一样,但 displayDiceFromRoll(diceRolls:numberOfSides:) 需要被当做交互的一部分进行测试。展示的方法全在 ViewControllerDisplay.swift 中,它是包含 ViewController 的一个extension的单独的文件。这是组织代码的一种方式,以保持 ViewController.swift 尽量得小,并将所有的展示方法收集到一个地方。

查看 ViewControllerDisplay.swift 文件,你会看到一堆私有方法和一个公有方法: displayDiceFromRoll(diceRolls:numberOfSides:) ,它可以清空展示的内容,填入文本信息,然后用一系列的子view来填充一个stack view,每个子view对应于一个骰子。

和所有的测试一样,在正确的地方开始非常重要。第一个测试就是检查结果text view和stack view在开始的时候是空的。

切到 ViewControllerTests.swift 并添加下列的测试:

func testDisplayIsBlankAtStart() {
XCTAssertEqual(vc.resultsTextView.string, "")
XCTAssertEqual(vc.resultsStackView.views.count, 0)
}

运行测试来确认开始的展示如同期望一般。

接下来,添加下面的测试来检查Roll按钮被点击之后,数据是否出现:

func testDisplayIsFilledInAfterRoll() {
vc.rollButtonClicked(vc.rollButton)
XCTAssertNotEqual(vc.resultsTextView.string, "")
XCTAssertEqual(vc.resultsStackView.views.count, 2)
}

由于默认的设置中,骰子的数量为2,因此检查stack view有两个view是安全的。但如果你不知道设置是什么,你就无法测试展示的数据是否正确了。

查看 ViewController.swift 中的 rollButtonClicked(_:) 。看看它是如何滚动骰子,然后来展示结果?如果你直接使用已知的数据调用 displayDiceFromRoll(diceRolls:numberOfSides:) 呢?这将允许精确地检查展示。

添加下列的测试到 ViewControllerTests.swift 中:

func testTextResultDisplayIsCorrect() {
let testRolls = [1, 2, 3, 4, 5, 6]
vc.displayDiceFromRoll(diceRolls: testRolls)
var expectedText = "Total rolled: 21\n"
expectedText += "Dice rolled: 1, 2, 3, 4, 5, 6 (6 x 6 sided dice)\n"
expectedText += "You rolled: 1 x 1s,  1 x 2s,  1 x 3s,  1 x 4s,  1 x 5s,  1 x 6s"
XCTAssertEqual(vc.resultsTextView.string, expectedText)
}

运行以验证测试结果如同预期中一样。即6个六面的骰子各自展示可能的一个面。

stack view会以一个更加图形化的方式来展示结果,如果可能的话,则使用骰子emoji。将下列的测试插入到 ViewControllerTests.swift 中:

func testGraphicalResultDisplayIsCorrect() {
let testRolls = [1, 2, 3, 4, 5, 6]
vc.displayDiceFromRoll(diceRolls: testRolls)
let diceEmojis = ["\u{2680}", "\u{2681}", "\u{2682}", "\u{2683}", "\u{2684}", "\u{2685}" ]
XCTAssertEqual(vc.resultsStackView.views.count, 6)
for (index, diceView) in vc.resultsStackView.views.enumerated() {
guard let diceView = diceView as? NSTextField else {
XCTFail("View (index) is not NSTextField")
return
}
let diceViewContent = diceView.stringValue
XCTAssertEqual(diceViewContent, diceEmojis[index], "View (index) is not showing the correct emoji.")
}
}

再次运行测试,以检查交互是否如你所预期一样地执行。剩下的两个测试将展示测试中一个非常有用的技术,通过提供已知的数据给一个方法,再检查结果。

如果你感到好奇,有一些重构看起来可以在这里完成!:]

UI测试

是时候去进行UI测试了。关闭assistant editor并打开 High_RollerUITests.swift 。默认的代码非常类似于目前为止你所看到的测试代码,仅仅是在 setup() 中额外的几行有所不同。

关于UI测试的一件很有趣的事,就是它可以记录界面的交互。从 testExample() 中移除注释,将光标置于方法内的空白行上,并点击编辑面板左下角的红点以开始录制:

Record-UI-Test

当app启动的时候,跟随这个交互的顺序,在每一个步骤后稍停一下,让Xcode至少写下一行新的代码:

  1. 点击“骰子数量”stepper的向上的箭头。
  2. 再次点击“骰子数量”stepper的向上的箭头。
  3. 双击“骰子数量”内的text field。
  4. 输入6并按下Tab键。
  5. 打开“面数”下拉菜单并选择12。
  6. 点击“Roll”按钮。

再次点击记录按钮以停止记录。

Xcode将在该方法中,自动生成对应你的操作的动作。但当你从下拉菜单中选择12的时候,会看到一些奇怪的事情的发送。Xcode无法确定使用哪个选项,因此会将可能的选项都展示给你。在一个复杂的界面中,这个机制对于区分不同的控件是非常重要的,但在本例中第一个选择就可以满足你的需求。

Record-UI-Test2

点击 menuItems[“12”] 旁的向下箭头以查看下拉菜单。选择一项来用足够得容易,但使Xcode相信你的选择并非如此得简单。

选择列表中第一个选项,下拉的菜单会消失。然后点击其中一项,这时它仍然会有淡蓝色的背景。当它被选中之后,背景就会带有一个略深一些的蓝色阴影,你可以按Return键来接收这个选择,之后你的代码看起来就类似如下的样子:

  func testExample() {
let highRollerWindow = XCUIApplication().windows["High Roller"]
let incrementArrow = highRollerWindow.steppers.children(matching: .incrementArrow).element
incrementArrow.click()
incrementArrow.click()
let textField = highRollerWindow.children(matching: .textField).element
textField.doubleClick()
textField.typeText("6\t")
highRollerWindow.children(matching: .popUpButton).element.click()
highRollerWindow.menuItems["12"].click()
highRollerWindow.buttons["Roll"].click()
}

记录的主要用途,是为了展示访问界面元素的语法。但意料之外的事却是你无法获取到 NSButton NSTextField 的引用,只能通过获取 XCUIElement 来代替。以此来赋予你发送消息和测试一些property的能力。 value 则是 Any 类型的,用来持有 XCUIElement 中最重要的内容。

使用记录中的信息来确定如何访问元素。此测试方法将用来检查使用stepper编辑骰子的数量后,text field中的值也可以发生相应的改变:

func testIncreasingNumberOfDice() {
let highRollerWindow = XCUIApplication().windows["High Roller"]
let incrementArrow = highRollerWindow.steppers.children(matching: .incrementArrow).element
incrementArrow.click()
incrementArrow.click()
let textField = highRollerWindow.children(matching: .textField).element
let textFieldValue = textField.value as? String
XCTAssertEqual(textFieldValue, "4")
}

保存文件,并点击旁边的小菱形来运行测试。app会运行起来,鼠标指针这时会移动到stepper的向上箭头处,并点击两次。这很有趣,就好像是一个机器人在操作你的app!

robot

这比之前的测试会慢很多,但UI测试并非是每天都会被使用的。

网络和异步测试

目前为止,每个人都是高兴的。家庭游戏之夜得以继续,角色扮演的朋友可以滚动所有奇怪的骰子,你的测试也证明了所有的一切都可以正确地工作...但总有些人会造成麻烦:

“我仍然不相信你的app可以滚动骰子。我发现了一个可以使用大气噪声来滚动骰子的网页。我希望你的app可以使用它。”

头疼。前往 Random.org 来查看它如何工作。如果URL包含有一个 num 参数,这个网页就会展示滚动很多六面的骰子的结果。似乎和下面这部分的内容相关:

<p>You rolled 2 dice:</p>
<p>
<img src="dice6.png" alt="6" />
<img src="dice1.png" alt="1" />
</p>

所以你就可以解析返回的数据,并将其用于骰子的滚动上。打开 WebSource.swift ,你就可以看到它到底是如何实现的。但你如何测试这点?

第一件事就是创建 WebSourceTests.swift 测试文件。选择 File Navigator 中的 High RollerTests 这组,并使用 File/New/File… 中的 macOS/Unit Test Case Class 创建一个名为 WebSourceTests 的类。

删除类的内容,并添加下列的import语句:

@testable import High_Roller

在assistant editor中打开 WebSource.swift

WebSource.swift 中查看 findRollOnline(numberOfDice:completion:) 方法。这个方法会创建一个 URLRequest 和一个 URLSession ,然后将它们连接到 URLSessionDataTask 中,它会尝试基于选定骰子的数量,尝试下载相应的网页。

收到数据后,它就会解析结果并调用completion handler,并带有骰子的结果或空数组作为参数。

作为测试的第一个尝试,添加下列的内容到 WebSourceTests.swift 中:

func testDownloadingOnlineRollPage() {
let webSource = WebSource()
webSource.findRollOnline(numberOfDice: 2) { (result) in
XCTAssertEqual(result.count, 2)
}
}

当你运行测试的时候,它会以令人怀疑的速度快速通过。点击左边的位置来添加一个断点在 XCTAssertEqual() 这行。

breakpoint

再次运行测试,你会发现断点永远不会触发。测试在没有结果返回的情况下就完成了。无需担心,XCTests有办法解决这点,那就是expectation!

将之前的测试替换为如下这样:

func testDownloadingPageUsingExpectation() {
// 1
let expect = expectation(description: "waitForWebSource")
var diceRollsReceived = 0
let webSource = WebSource()
webSource.findRollOnline(numberOfDice: 2) { (result) in
diceRollsReceived = result.count
// 2
expect.fulfill()
}
// 3
waitForExpectations(timeout: 10, handler: nil)
XCTAssertEqual(diceRollsReceived, 2)
}

这里有几件新鲜的事值得去关注:

  1. 用人类可读的描述创建一个 XCTestExpectation
  2. 当数据返回之后,闭包就会被调用,并通过指明现在等待的事情来实现这个期望。
  3. 为这个测试方法设置一个超时时间,以等待期望被实现。在本例中,如果网页不能在10秒之内返回数据,期望就会超时。

这次,在 XCTAssertEqual() 这行设置一个断点,它应当会触发,测试就可以真正地通过了。如果你想看到一个期望超时时会发生什么,可以将超时时间设置为一个非常小的值(比方说0.1秒),并再次运行测试。

现在你就了解如何进行异步的测试了,这对于测试网络连接,和长时间的后台任务非常得有用。但如果在未连接到网络的情况下,你想测试网络的代码,或是这个网站已关闭,或是你只想测试地更快一些,该怎么办?

在本例中,你可以使用一种叫做 mocking 的测试技术,来模拟你的网络调用。

Mocking

在实际的代码中, URLSession 是用来启动 URLSessionDataTask 的,用它来返回响应。由于不想访问网络,你可以测试 URLRequest 是正确配置的,而 URLSessionDataTask 可以被创建, URLSessionDataTask 可以被启动。

你将要创建的mock版本的类包括: MockURLSession MockURLSessionDataTask ,你可以用它们来代替真实的类。

WebSourcesTests.swift 文件的底部, WebSourceTests 类的外部,添加下面的两个新的类:

class MockURLSession: URLSession {
var url: URL?
var dataTask = MockURLSessionTask()
override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> MockURLSessionTask {
self.url = request.url
return dataTask
}
}
class MockURLSessionTask: URLSessionDataTask {
var resumeGotCalled = false
override func resume() {
resumeGotCalled = true
}
}

MockURLSession 继承自 URLSession ,提供了一个替代版本的 dataTask(with:completionHandler:) ,它会储存来自提供的 URLRequest 的URL并返回一个 MockURLSessionTask 来替代 URLSessionDataTask

MockURLSessionTask 则继承自 URLSessionDataTask ,当 resume() 被调用时,并不会真正地访问网络,而是标记一个flag表示此事已发生。

添加下列代码到 WebSourceTests 类中,并运行新的测试:

func testUsingMockURLSession() {
// 1
let address = "https://www.random.org/dice/?num=2"
guard let url = URL(string: address) else {
XCTFail()
return
}
let request = URLRequest(url: url)
// 2
let mockSession = MockURLSession()
XCTAssertFalse(mockSession.dataTask.resumeGotCalled)
XCTAssertNil(mockSession.url)
// 3
let task = mockSession.dataTask(with: request) { (data, response, error) in }
task.resume()
// 4
XCTAssertTrue(mockSession.dataTask.resumeGotCalled)
XCTAssertEqual(mockSession.url, url)
}

这个测试中做了些什么?

  1. 如同之前一样地构建 URLRequest
  2. 创建一个 MockURLSession 并配置初始的property。
  3. 创建 MockURLSessionTask 并调用 resume()
  4. 测试这些property发生了如预期中的变化。

这就完成了第一部分的测试: URLRequest URLSession ,和 URLSessionDataTask ,以及data task的启动。现在缺少的测试是解析返回的数据。

这里你需要覆盖两个测试的case:返回的数据是否为期望中的格式。

添加下列的两个测试到 WebSourcesTests.swift 中并运行:

func testParsingGoodData() {
let webSource = WebSource()
let goodDataString = "<p>You rolled 2 dice:</p>\n<p>\n<img src="dice6.png" alt="6" />\n<img src="dice1.png" alt="1" />\n</p>"
guard let goodData = goodDataString.data(using: .utf8) else {
XCTFail()
return
}
let diceArray = webSource.parseIncomingData(data: goodData)
XCTAssertEqual(diceArray, [6, 1])
}
func testParsingBadData() {
let webSource = WebSource()
let badDataString = "This string is not the expected result"
guard let badData = badDataString.data(using: .utf8) else {
XCTFail()
return
}
let diceArray = webSource.parseIncomingData(data: badData)
XCTAssertEqual(diceArray, [])
}

这里你使用期望来测试了网络连接,mocking来模拟网络,来使得测试可以独立于网络和第三方的网站,最后则提供数据来测试数据的解析,增强独立的能力。

性能测试

Xcode还提供了性能的测试,来检测你代码运行的速度。在 Roll.swift 中的 totalForDice() ,使用 flatMap reduce 来计算骰子的总数,且允许 value 是可选类型的。但这是最快的方法么?

为了测试性能,在 File Navigator 中选择 High RollerTests 这组,并使用 File/New/File... 中的 macOS/Unit Test Case Class 创建一个名为 PerformanceTests 的类。

如你所想,和之前一样,删除类中的内容,并添加下列的import:

@testable import High_Roller

插入下列的测试方法:

  func testPerformanceTotalForDice_FlatMap_Reduce() {
// 1
var roll = Roll()
roll.changeNumberOfDice(newDiceCount: 20)
roll.rollAll()
// 2
self.measure {
// 3
 = roll.totalForDice()
}
}

上述的测试:

  1. 用20个 Dice 设置 Roll
  2. self.measure 则定义了定时的block。
  3. 这里就是将被测量的代码。

运行测试,你将会看到类似如下的结果:

PerformanceTest

在获得绿色的勾的标记的同时,你还会看到一个速度的标识,显示“Time: 0.000 sec (98% STDEV)”。 STDEV(标注偏差)会表明这里和之前的结果是否存在有重大的差别。在本例中,这里就一个结果 - 0 - 因此STDEV是无意义的。无意义的结果就是0.000秒了,因此需要更长的测试。要做到这点,最简单的方式就是添加循环来重复measure block足够多的次数,以此获取到实际的时间。

将测试替换为如下的代码:

  func testPerformanceTotalForDice_FlatMap_Reduce() {
var roll = Roll()
roll.changeNumberOfDice(newDiceCount: 20)
roll.rollAll()
self.measure {
for  in 0 ..< 10_000 {
_ = roll.totalForDice()
}
}
}

再次运行测试。你得到的结果会依赖于你的处理器,在我这里大约是0.2秒。可以调整循环的次数,让你最后所得到的时间接近0.2秒。

还有三种可能的方式来增加total。在assistant editor中打开 Roll.swift 并添加下列的代码:

  func totalForDice2() -> Int {
let total = dice
.filter { $0.value != nil }
.reduce(0) { $0 + $1.value! }
return total
}
func totalForDice3() -> Int {
let total = dice
.reduce(0) { $0 + ($1.value ?? 0) }
return total
}
func totalForDice4() -> Int {
var total = 0
for d in dice {
if let dieValue = d.value {
total += dieValue
}
}
return total
}

下面则是相应的性能测试,你可以将它们添加到 PerformanceTests.swift 中:

  func testPerformanceTotalForDice2_Filter_Reduce() {
var roll = Roll()
roll.changeNumberOfDice(newDiceCount: 20)
roll.rollAll()
self.measure {
for  in 0 ..< 10_000 {
 = roll.totalForDice2()
}
}
}
func testPerformanceTotalForDice3_Reduce() {
var roll = Roll()
roll.changeNumberOfDice(newDiceCount: 20)
roll.rollAll()
self.measure {
for  in 0 ..< 10_000 {
 = roll.totalForDice3()
}
}
}
func testPerformanceTotalForDice4_Old_Style() {
var roll = Roll()
roll.changeNumberOfDice(newDiceCount: 20)
roll.rollAll()
self.measure {
for  in 0 ..< 10_000 {
 = roll.totalForDice4()
}
}
}

运行这些测试,看下那种方式是最快的。你能猜出那种是最快的么?反正我不能!

tortoise

代码覆盖

我们要讨论的最后一个Xcode测试工具就是代码覆盖了,它会用来统计这一系列的测试覆盖了你多少的代码。默认它是关闭的,要将其打开,只需在窗口顶部的schemes下拉菜单中选择 Edit Scheme… ,然后选择左侧一列的 Test 并勾选 Gather coverage data

Turn-On-Code-Coverage

关闭这个窗口并按下 Command-U 键来重新运行所有的测试。完成之后,切到 Report Navigator 并选择latest这项。

你会看到测试报告展示了一系列绿色的勾的标记,以及一些性能测试的计时。如果你无法看到这些,请确认在左上角选择了 All 开关。

点击这里顶部的 Coverage ,并使鼠标经过蓝条之上,你会看到你的测试覆盖了你将近80%的代码。令人惊奇的工作!:]

Code-Coverage-4

两个model对象( Dice Roll )都被很好地覆盖了。如果你只想添加一些测试,model就是开始的最好的地方。

还有一种很好的,并且可以快速提高代码覆盖率的方法:删除未使用过的代码。如 AppDelegate.swift ,它的代码覆盖率仅为50%。

切到 AppDelegate.swift 。在右侧的“沟”上,上下移动鼠标,你会看到在测试过程中调用过的代码被标记成了绿色,未调用过的则标记成了红色。

Uncovered-code

在本例中, applicationWillTerminate(_:) 方法从未被使用过,但却显著地减小了代码覆盖率。由于本app用不到这个方法,就可以将它直接删除。再次运行所有的测试, AppDelegate.swift 就变到100%了。

这个看起来像是在骗系统,但移除任何会使你app变杂乱的无用代码确实是一个很好的实践。Xcode为了使用方便,会在新建文件时提供很多的模板代码,但如果你不需要的话,删除即可。

注意: 如果你觉得代码的“沟”,和红色绿色的标记会让你分心,可以通过 Editor 菜单中的 Hide Code Coverage 来将它们关闭。这并不会让Xcode停止收集代码覆盖的数据,只是让它在你编辑的时候先不要展示出来。

关于代码覆盖有一些小小的提示:它只是一个工具,而不是一个目标!一些开发者会将它看做是一个目标,必须保持一个很高的代码覆盖率才可以,但也有可能是通过无意义的测试来获得的较高百分比。测试应当要经过很好的考虑,而不是仅仅为了增加你的代码覆盖率。

测试可能在调用了你大量代码的情况下,并没有实际地检查它们的结果是否正确。尽管一个较高的代码覆盖率的数据可能会比较低的好一些,但这并不能完全地代表了代码的质量就会更好。

从这儿去向哪里?

你可以在 这里 下载最终版本的项目。

关于使用Xcode测试,苹果有一系列的 文档 及相关的WWDC视频。

NSHipster 中有关于各种断言,以及编写测试你需要知道的地方非常有用的总结。

关于TDD的更多信息,可以参考 Uncle Bob’s excellent site

如果感兴趣于UITests的更多内容?请访问 Joe Masilotti’s excellent cheat sheet