原文地址 翻译:DeveloperLx
在本教程的 第一部分 ,你已经学习了如何使用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
}
上述的代码:
-
整个的类会使用一个名为
File Navigator
的property访问
ViewController
。可以将它设为非可选的,因为如果它崩溃掉的话,仍然是一个非常有用的测试结果。 -
这个view controller是从storyboard的
setup()
中实例化的。 -
为了触发view的生命周期,获取view controller的
view
property。你无需去保存,因为获取它的动作就已是的创建view controller的动作正确地执行。
以这种方式实例化view controller,必须保证它有一个storyboard ID。打开 Main.storyboard ,选择 ViewController ,并在右侧打开 Identity Inspector 。将Storyboard ID设置为 ViewController 。
第一个测试将用来确认 ViewController 正确地被创建。切回 ViewControllerTests.swift 并添加下列的测试方法:
func testViewControllerIsCreated() {
XCTAssertNotNil(vc)
}
运行测试。如果出现失败或崩溃的话,返回 Main.storyboard 并检查你的storyboard ID是否设置正确。
运行app来查看一下界面。所有的控件都是有功能的,因此点击 Roll 按钮来滚动骰子;修改设置并再次滚动,注意骰子的数量可以使用一个text field或stepper来设定,而骰子的面数则使用一个弹出菜单来设定。
在测试之前,控件的操作都如同期望中一样地执行,你需要首先确认交互元素是以期望中的值开始的。
添加下列的测试到 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(_:)
。它做了三件事:
- 确保任何正在骰子text field中编辑的值都被处理过。
-
告知
Roll
结构体滚动所有骰子。 - 展示结果。
你早已编写了测试来确认
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测试了。关闭assistant editor并打开
High_RollerUITests.swift
。默认的代码非常类似于目前为止你所看到的测试代码,仅仅是在
setup()
中额外的几行有所不同。
关于UI测试的一件很有趣的事,就是它可以记录界面的交互。从
testExample()
中移除注释,将光标置于方法内的空白行上,并点击编辑面板左下角的红点以开始录制:
当app启动的时候,跟随这个交互的顺序,在每一个步骤后稍停一下,让Xcode至少写下一行新的代码:
- 点击“骰子数量”stepper的向上的箭头。
- 再次点击“骰子数量”stepper的向上的箭头。
- 双击“骰子数量”内的text field。
- 输入6并按下Tab键。
- 打开“面数”下拉菜单并选择12。
- 点击“Roll”按钮。
再次点击记录按钮以停止记录。
Xcode将在该方法中,自动生成对应你的操作的动作。但当你从下拉菜单中选择12的时候,会看到一些奇怪的事情的发送。Xcode无法确定使用哪个选项,因此会将可能的选项都展示给你。在一个复杂的界面中,这个机制对于区分不同的控件是非常重要的,但在本例中第一个选择就可以满足你的需求。
点击 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!
这比之前的测试会慢很多,但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()
这行。
再次运行测试,你会发现断点永远不会触发。测试在没有结果返回的情况下就完成了。无需担心,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)
}
这里有几件新鲜的事值得去关注:
-
用人类可读的描述创建一个
XCTestExpectation
。 - 当数据返回之后,闭包就会被调用,并通过指明现在等待的事情来实现这个期望。
- 为这个测试方法设置一个超时时间,以等待期望被实现。在本例中,如果网页不能在10秒之内返回数据,期望就会超时。
这次,在
XCTAssertEqual()
这行设置一个断点,它应当会触发,测试就可以真正地通过了。如果你想看到一个期望超时时会发生什么,可以将超时时间设置为一个非常小的值(比方说0.1秒),并再次运行测试。
现在你就了解如何进行异步的测试了,这对于测试网络连接,和长时间的后台任务非常得有用。但如果在未连接到网络的情况下,你想测试网络的代码,或是这个网站已关闭,或是你只想测试地更快一些,该怎么办?
在本例中,你可以使用一种叫做 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)
}
这个测试中做了些什么?
-
如同之前一样地构建
URLRequest
。 -
创建一个
MockURLSession
并配置初始的property。 -
创建
MockURLSessionTask
并调用resume()
。 - 测试这些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()
}
}
上述的测试:
-
用20个
Dice
设置Roll
。 -
self.measure
则定义了定时的block。 - 这里就是将被测量的代码。
运行测试,你将会看到类似如下的结果:
在获得绿色的勾的标记的同时,你还会看到一个速度的标识,显示“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()
}
}
}
运行这些测试,看下那种方式是最快的。你能猜出那种是最快的么?反正我不能!
我们要讨论的最后一个Xcode测试工具就是代码覆盖了,它会用来统计这一系列的测试覆盖了你多少的代码。默认它是关闭的,要将其打开,只需在窗口顶部的schemes下拉菜单中选择 Edit Scheme… ,然后选择左侧一列的 Test 并勾选 Gather coverage data 。
关闭这个窗口并按下 Command-U 键来重新运行所有的测试。完成之后,切到 Report Navigator 并选择latest这项。
你会看到测试报告展示了一系列绿色的勾的标记,以及一些性能测试的计时。如果你无法看到这些,请确认在左上角选择了 All 开关。
点击这里顶部的 Coverage ,并使鼠标经过蓝条之上,你会看到你的测试覆盖了你将近80%的代码。令人惊奇的工作!:]
两个model对象(
Dice
和
Roll
)都被很好地覆盖了。如果你只想添加一些测试,model就是开始的最好的地方。
还有一种很好的,并且可以快速提高代码覆盖率的方法:删除未使用过的代码。如 AppDelegate.swift ,它的代码覆盖率仅为50%。
切到 AppDelegate.swift 。在右侧的“沟”上,上下移动鼠标,你会看到在测试过程中调用过的代码被标记成了绿色,未调用过的则标记成了红色。
在本例中,
applicationWillTerminate(_:)
方法从未被使用过,但却显著地减小了代码覆盖率。由于本app用不到这个方法,就可以将它直接删除。再次运行所有的测试,
AppDelegate.swift
就变到100%了。
这个看起来像是在骗系统,但移除任何会使你app变杂乱的无用代码确实是一个很好的实践。Xcode为了使用方便,会在新建文件时提供很多的模板代码,但如果你不需要的话,删除即可。
关于代码覆盖有一些小小的提示:它只是一个工具,而不是一个目标!一些开发者会将它看做是一个目标,必须保持一个很高的代码覆盖率才可以,但也有可能是通过无意义的测试来获得的较高百分比。测试应当要经过很好的考虑,而不是仅仅为了增加你的代码覆盖率。
测试可能在调用了你大量代码的情况下,并没有实际地检查它们的结果是否正确。尽管一个较高的代码覆盖率的数据可能会比较低的好一些,但这并不能完全地代表了代码的质量就会更好。
你可以在 这里 下载最终版本的项目。
关于使用Xcode测试,苹果有一系列的 文档 及相关的WWDC视频。
NSHipster 中有关于各种断言,以及编写测试你需要知道的地方非常有用的总结。
关于TDD的更多信息,可以参考 Uncle Bob’s excellent site 。
如果感兴趣于UITests的更多内容?请访问 Joe Masilotti’s excellent cheat sheet 。