(译)iOS 单元测试和界面测试教程 (已完成)

<p> 原文地址:https://www.raywenderlich.com/150073/ios-unit-testing-and-ui-testing-tutorial (March 13, 2017) </p>

<p>虽然写测试程序不是一件容易令人着迷的事情,但它却是非常必要的。因为测试可以避免你炫酷的应用成为一个错误百出的垃圾。如果你正在阅读本教程,你应该已经意识到需要编写测试程序来测试你的代码和界面了,但你可能还不知道如何在Xcode中进行测试。</p>
<p>也许你已经有了一个可以“工作”的应用程序,但还没有为它建立测试程序,而且你希望在扩展你的应用时测试可以覆盖所有的改动。也许你已经写了一些测试程序,但不确定它们是否正确。又或者你正在写一个应用,希望能能够同步进行测试。</p>
<p>这个教程展示了如何使用Xcode中的测试导航来测试你的应用模型和异步方法;如何使用存根(stub)和模拟对象来和库或者系统对象进行互动;如何测试应用的用户界面和性能,以及如何使用代码覆盖工具。在这个过程中你会遇到一些测试高手的常用词汇。在本教程的结尾,你将使用aplomb工具来将依赖注入到你的被测试系统中去!</p>
<p>测试,测试…</p>

什么是测试?

<p>在开始写任何测试程序之前,先考虑一个最基础的问题:你要测什么?如果你的目标是扩展现有的应用程序,你应该首先为你打算修改的组件编写测试。</p>
<p>具体来说,测试应该覆盖以下内容:</p><p><li>核心功能:模型类和方法,以及它们与控制器的交互
<li>最常用到的用户操作流程
<li>边界条件
<li>要解的bug</p>

FIRST: 测试的最佳实践

<p>缩写FIRST描述了一套简洁有效的单元测试标准。这些标准是:</p><li>快(F):测试应该执行的尽可能快,这样人们就不会介意运行它们。
<li>独立/隔离(I):测试应该相互独立,有各自的建立(setup)和拆卸(teardown)过程。
<li>重复性(R):每次运行测试都应获得相同的结果。外部数据和并发问题会可能导致间歇性故障。
<li>自我验证(S):测试应该是全自动的;输出的结果要么是“通过”,要么是“失败”,而不能是输出一个只有程序员看得懂的日志文件。
<li>及时(T):理想情况下,应该先写测试代码再写对应的生产代码。
<p>遵循FIRST原则,将使你的测试清晰明了、并且真的可以对开发有帮助,而不是成为应用开发的阻碍。</p>

让我们开始吧

<p>下载,解压,打开并查看两个准备好了的起始项目 BullsEye 和 halftunes。</p>
<p>BullsEye 是基于“iOS学徒”教学中的一个示例应用;我已经将其中的游戏逻辑提取到bullseyegame类中并添加了一种新的玩法。在右下角的分段控件可以让用户选择玩法:要么是移动滑块来尽可能地接近目标值;或者是通过猜测滑块的位置来得分。用户当前的玩法选择将被作为默认值存储起来。</p>
<p>Halftuness是来自 NSURLSession教程中的示例应用程序,代码已经被更新到了Swift 3。用户可以通过iTunes API查询歌曲,然后下载和播放歌曲的试听片段。(译注:这个项目对新手可能会复杂些,所以我写了个分析。)</p>
<p>让我们开始测试吧!</p>

Xcode中的单元测试

创建一个单元测试的目标

Xcode中的测试导航栏提供了非常简便的进行测试的方法;你将使用它来创建测试目标和为你的应用执行测试程序。
打开BullsEye工程,按⌘+5切换到测试导航栏。
单击左下角的“+”按钮,然后从菜单选择 ”新的单元测试目标…:

接受默认名称BullsEyeTests。当测试目标在测试导航栏中出现后,单击它在编辑器中打开。如果BullsEyeTests没有自动出现,试试切换到另一个导航栏,再切回。


<p>自动生成的测试类模板会导入<code>XCTest</code>并定义一个XCTestCase的子类<code>BullsEyeTests</code>,以及setup(),teardown()和示例测试方法。有三种方法可以运行测试类:
</p><ol>
<li>从菜单上选择Product->Test 或者按 ⌘+U。 这会执行所有的测试类。</li>
<li>点击测试导航栏中的箭头按钮。</li>
<li>点击分隔栏上的菱形标志。</li>
</ol>

<p>您也可以点击每个测试方法所对应的菱形标志来执行该方法,无论是在测试导航栏还是在分隔栏上点都可以。</p>
<p>尝试不同的方法来执行测试,感受一下所需的时间,还有它们看起来像什么。因为现在测试程序还没有做任何事,所以执行会很快!</p>
<p>当所有的测试都通过后,菱形标志将变绿,并显示一个对号。点击在testPerformanceExample()方法结尾处的灰色菱形按钮打开性能结果展示:</p>

<code>testPerformanceExample()</code>这个方法用不到,可以删除。</p>

使用<code>XCTAssert</code>测试模型

<p>首先,你将使用XCTAssert测试BullEye的一个核心功能:BullsEyeGame对象在每一轮中计算出的分数是否都正确?打开BullsEyeTests.swift,在import语句的下方加入这一行:</p>
<p><pre><code>@testable import BullsEye</p></code></pre><p>这使得测试类可以访问BullsEye中的类和方法。在BullsEyeTests类的顶部,添加属性:</p>
<p><pre><code>var gameUnderTest: BullsEyeGame!</p></code></pre><p>在setup()中创建一个新的BullsEyeGame对象,放在对super的调用的后面:</p>
<p><pre><code>gameUnderTest = BullsEyeGame()</code>
<code>gameUnderTest.startNewGame()</code></pre><p>这将在类中创建一个SUT(被测试系统)对象,所以在这个测试类中的所有测试方法都可以访问该SUT对象的属性和方法。</p>
在这里,你还可以调用游戏的startNewGame方法,创建一个目标值。你的许多测试都将使用该目标值来检验游戏是否正确地计算了得分。
<p>在你忘记之前,在<code>tearDown()</code>中释放该SUT对象,将下面的代码放在对super的调用之前:</p>
<pre>gameUnderTest=nil</pre>
<pre>注:在setup() 中建立SUT并在tearDown()中释放它是一个最佳实践。这可以确保每一个测试在一个干净的状态中开始。乔恩瑞德写的相关文章中有更多的讨论。</pre>
现在你已经准备好写你的第一个测试方法了!使用下面的代码替换testExample()
<pre>// 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") }</pre>
测试方法的名字总是以test开始,紧跟着的是对该测试方法的描述。
以假设(given), 当(when) 和然后(then)三段的方式来组织一个测试方法是一个很好的做法:
在given部分,设置任何测试所需要的值:在这个例子中创建了一个猜测值,并指定它和目标值的差。
在when部分,执行被测试代码:执行gameUndertest.check(_:)
在then部分,使用断言来判断结果(在这个例子中,gameUnderTest.scoreRound 的值是 100-5=95)。如果测试失败则会打印出一个消息。
通过在分隔栏或测试导航器中单击菱形标志来执行测试。应用程序将会被构建和执行,然后菱形标志会变成绿色的对号!
注:要想查看完整的XCTestAssertions列表,按下 ⌘ 并点击XCTAssertEqual来打开 XCTestAssertions.h,或去查看苹果的官方文档

注:测试的Given-When-Then结构起源于行为驱动开发(BDD)。这是一种对客户友好,术语简单的命名方式。其它的命名系统还有Arrange-Act-Assert (组织-行动-断言)和Assemble-Activate-Assert(组装-激活-断言)。</p>

对测试进行调试

我在BullsEyeGame中故意放置了一个bug,现在你去把它找出来。要发现这个bug,将testScoreIsComputed方法重命名为testScoreIsComputedWhenGuessGTTarget,然后复制、粘贴建立一个新方法testScoreIsComputedWhenGuessLTTarget
在这个测试方法中,在given段,将.targetValue+5 改为-5。其它部分不变:
<pre>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") }</pre>
guesstargetValue之间的差仍然是5,所以得分应该还是95。
在断点导航栏中,添加一个测试失败断点;当测试方法发出失败断言时,测试将停止执行。

<p>执行测试:它应该停在XCTAssertEqual处,并伴随一个测试失败的提示。在调试控制台检查gameUnderTestguess变量的值:</p>

guess 等于 targetValue - 5 但得分(scoreRound)是105,不是95! (译注:targetValue是随机产生的,因而你看到的值很可能会和图中的不同)
<p>为了进一步调查,我们采取通常的调试过程:在when陈述与BullsEyeGame.swift中导致了差异的check(_:)处分别设置一个断点。然后再次执行行测试,断点生效后单步跳过赋值行来检查应用中difference变量的值:</p>

我们发现,问题出在difference是负值,所以得分是 100-(-5) ;修复方法是使用绝对值。在check(_:)中,打开正确行,删除错误行。
<p>移除这两个断点并再次执行测试以确认它现在是成功的了。</p>

使用 XCTestExpectation 测试异步操作

<p>现在,你已经学会了如何测试模型和调试测试失败的情况。下面让我们继续使用XCTestExpectation来测试网络操作。</p><p>
打开HalfTunes工程:它利用URLSession来调用iTunes API查询和下载歌曲的试听片段。假设你想修改它,改用AlamoFire来进行网络操作。要查看改变是否会引入任何问题,您应该为网络操作编写测试方法并在该换为AlamoFire的前后运行它们。</p><p>
URLSession方法是异步调用:它们会立刻返回,但真正结束运行还要等上一段时间。为了测试异步方法,使用XCTestExpectation方法使你的测试等待异步操作完成。</p>
<p>异步测试通常是比较花时间的,所以应该将它们与那些可以很快就执行完的测试方法分开。</p>
<p>从+菜单选择"新建单元测试目标…",将测试命名为HalfTunesSlowTests。在import声明后导入HalfTunes:</p>
<pre><code>@testable import HalfTunes</code></pre>
<p>在这个类的测试中将使用默认会话来将请求发送给苹果的服务器,所以声明一个SUT对象,并在setup()方法中创建它,在teardown()方法中释放它:</p>

<pre>var sessionUnderTest: URLSession! override func setUp() { super.setUp() sessionUnderTest = URLSession(configuration: URLSessionConfiguration.default) } override func tearDown() { sessionUnderTest = nil super.tearDown() }</pre>用下面的异步测试方法替换<code>testExample()</code>:
<pre>// Asynchronous test: success fast, failure slow func testValidCallToiTunesGetsHTTPStatusCode200() { // given let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") // 1 let promise = expectation(description: "Status code: 200") // when let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in // then if let error = error { XCTFail("Error: \(error.localizedDescription)") return } else if let statusCode = (response as? HTTPURLResponse)?.statusCode { if statusCode == 200 { // 2 promise.fulfill() } else { XCTFail("Status code: \(statusCode)") } } } dataTask.resume() // 3 waitForExpectations(timeout: 5, handler: nil) }</pre><p>这个测试方法发送一个有效的查询给iTunes并期望返回的状态代码是200。这里的大多数代码和在应用程序中编写的代码相同,除了下面这些额外的代码行:</p>
<ol><li>expectiation(_:) 返回一个XCTestExpectation对象,它被赋值给promise变量。该对象的其他常用名称是expectationfuture。参数description描述了期望发生的结果。
<li>为了和描述匹配,在异步方法完成处理函数的成功条件分支中调用promise.fulfill()
<li>waitForExpectations(_: handler:)保证测试在所有的期望被达成前保持运行,或者直到超时结束,以二者先发生者为准。</ol>
<p>执行测试。如果你连接到了互联网,在模拟器启动后,测试应该需要大约一秒钟成功返回。(译注:这个方法用URLSession执行了和程序中类似的操作,但没有直接测试程序中的代码)</p>

<h3>让失败发生得更快</h3>
<p>失败是有害的,但不必总保持失败。在这里将讨论如果测试失败了,如何快速找出原因。把时间节省下来可以更好地浪费在脸谱网上。:]</p>
<p>要修改测试,以使异步操作返回失败结果,只需要从网址中的“iTunes”里删除“s”
<pre><code>leturl=URL(string:"https://itune.apple.com/search?media=music&entity=song&term=abba")</code></pre>
执行测试:测试会返回失败,但它需要等待整个超时时间间隔!这是因为它的期望是请求会成功,就是在调用promise.fulfill()的地方。由于请求失败,测试只有在超时过期了才结束。(译注:虽然测试已经失败了(XCTFail()被调用),但因为promise.fulfill()没有被调用,方法不会立刻结束。)
<p>通过更改期望,可以使测试失败情况发生的更快,而不必等待请求成功。只要等到异步方法的完成处理方法被调用就可以了。也就是当应用程序收到来自服务器的响应(OK或错误)时。这满足了期望,然后测试里可以接着检查请求是否成功。</p>
<p>要查看这是如何工作的,你将创建一个新的测试。首先,撤销上面对URL的更改来修复测试,然后在类中添加下面的测试方法:</p>
<pre>```// Asynchronous test: faster fail
func testCallToiTunesCompletes() {
// given
let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
// 1
let promise = expectation(description: "Completion handler invoked")
var statusCode: Int?
var responseError: Error?

// when
let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
statusCode = (response as? HTTPURLResponse)?.statusCode
responseError = error
// 2
promise.fulfill()
}
dataTask.resume()
// 3
waitForExpectations(timeout: 5, handler: nil)
// then
XCTAssertNil(responseError)
XCTAssertEqual(statusCode, 200)
}</pre> 这里的关键是,单单进入完成处理程序就满足了期望,这需要大约一秒钟。如果请求失败了,则then部分的断言会失败。 <p>执行测试:它现在需要大约一秒钟就会失败返回。失败是因为查询请求失败了,而不是因为测试执行超时。 修复url```,然后再次执行测试并确认现在结果是成功的。</p>

伪对象和交互

<p>异步测试是为了确认代码中调用异步API的输入参数是正确的。你也可能还想要测试接收urlsession的返回值的代码也能正常工作,或者程序可以正确地更新UserDefaults或CloudKit数据库。</p><p>
大多数应用都要同系统或者库函数对象打交道,这些对象不受你控制。如果测试方法同这些对象进行交互,那么执行起来可能会很慢或者结果不具有可重复性。这就违反了FIRST原则中的两个,执行的速度要快和具有可重复性。使用输入桩(stubs)或通过更新模拟对象(mock objects)来伪造交互是常用的替代方法。</p>
当你的代码依赖于某个系统或库时,可以使用伪装,即创建一个伪对象来扮演相关的系统或库,并把它注入到你的代码中。Jon Reid写的依赖注入描述了几种可以达到这个目的的方法。

</p>

来自桩(stub)的伪输入

<p>在这个测试中,你会通过检查searchResults.count的值来判断程序的<code>updateSearchResults(_:)</code>方法是否正确地解析了会话所下载的数据。在这里,SUT是视图控制器,你会用桩和一些预先下载的数据来伪造会话。</p><p>
从+菜单选择“新的单元测试目标…”。把它命名为HalfTunesFakeTests。在import语句下方导入HalfTunes :</p>
<pre><code>@testable import HalfTunes</p></code></pre><p>声明SUT,在setup()中创建它,并在teardown()中释放:</p><pre>var controllerUnderTest: SearchViewController! override func setUp() { super.setUp() controllerUnderTest = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! SearchViewController! } override func tearDown() { controllerUnderTest = nil super.tearDown() }</pre>
<pre>注:这里SUT是视图控制器。因为HalfTunes有视图控制器过于庞大的问题-所有的工作都在SearchViewController.swift中进行。将网络代码移动到单独的模块可以减轻这个问题,也会使测试变得更容易。</pre><p>
接下来,你将需要产生一些JSON样本数据来由伪会话返回给你的测试方法。数据有几条就够了,所以在发给iTunes的URL字符串后面加上“& limit=3”来限制返回结果:</p>
<p>https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3</p>
<p>拷贝粘贴这个URL到浏览器中。这会下载1.txt或一个类似的文件。预览确认这是一个JSON文件,然后将它改名为abbaData.json并添加到HalfTunesFakeTests组中。</p>
<p>HalfTunes工程中包含支持文件DHURLSessionMock.swift。其中定义了一个简单的协议——DHURLSession。这个协议中包含两个使用URLURLRequest来创建数据任务的方法(stubs)。它还定义了一个实现了该协议的URLSessionMockURLSessionMock提供一个构造器,它可以根据你提供的数据(data, response, error)创建一个模拟URLSession对象。</p>
<p>如下所示,在setup()中创建SUT后,建立伪数据和响应,并建立伪会话对象。</p>
<pre>let testBundle = Bundle(for: type(of: self)) let path = testBundle.path(forResource: "abbaData", ofType: "json") let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped) let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") let urlResponse = HTTPURLResponse(url: url!, statusCode: 200, httpVersion: nil, headerFields: nil) let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)</pre><p>在setup()的结尾,将伪会话作为SUT的属性注入到应用程序中:</p>
<pre>controllerUnderTest.defaultSession = sessionMock</pre>
<pre>注:你将在你的测试中直接使用伪会话,但向你展示了如何进行注入,以便在未来的测试中可以调用SUT的方法来使用视图控制器的defaultSession属性。</pre><p>现在你准备好了写一个测试来检查对<code>updateSearchResults(_:)</code>的调用是否正确解析了所提供的伪数据。用下面的代码替换<code>testExample()</code>:</p>
<pre>// Fake URLSession with DHURLSession protocol and stubs func test_UpdateSearchResults_ParsesData() { // given let promise = expectation(description: "Status code: 200") // when XCTAssertEqual(controllerUnderTest?.searchResults.count, 0, "searchResults should be empty before the data task runs") let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") let dataTask = controllerUnderTest?.defaultSession.dataTask(with: url!) { data, response, error in // if HTTP request is successful, call updateSearchResults(\_:) which parses the response data into Tracks if let error = error { print(error.localizedDescription) } else if let httpResponse = response as? HTTPURLResponse { if httpResponse.statusCode == 200 { promise.fulfill() self.controllerUnderTest?.updateSearchResults(data) } } } dataTask?.resume() waitForExpectations(timeout: 5, handler: nil) // then XCTAssertEqual(controllerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response") }</pre>
<p>因为桩提供的是一个异步方法,这个测试也仍然必须写成异步的。(译注:这里的桩是sessionMock)</p>
<p>when断言的条件是searchResults在数据任务运行前为空-这应该是真的,因为你在setup()中创造了一个全新的SUT。</p>
<p>伪数据包含了三个音轨对象的JSON数据,所以then断言的条件是,视图控制器的搜索结果数组包含三个项目。</p>
<p>执行测试。它应该很快就返回成功,因为没有任何真正的网络连接。</p>

对模拟对象的假更新

<p>以前的测试使用存根从伪对象提供输入。接下来,你可以使用一个模拟对象来测试你的代码可以正确地更新UserDefaults。</p>
<p>重新打开BullsEye项目。该应用程序有两种玩法:用户要么移动滑块来匹配目标值或根据滑块位置猜测目标值。右下角的分段控件可以切换游戏玩法和更新gameStyle用户默认值以保持一致。</p>
<p>你的下一个测试将检查应用程序正确地更新了gameStyle的默认值。</p>
<p>在测试导航栏,点击“新的单元测试的目标”,将测试命名为BllsEyeMockTests。在import语句下面添加以下内容:</p>
<pre>@testable import BullsEye class MockUserDefaults: UserDefaults { var gameStyleChanged = 0 override func set(_ value: Int, forKey defaultName: String) { if defaultName == "gameStyle" { gameStyleChanged += 1 } } }</pre>
MockUserDefaults覆盖了set(_:forKey:)方法来增大gameStyleChanged标志。你经常会看到类似的测试中设置的是布尔变量,但使用递增整数可以给你更多的灵活性,例如,您的测试可以检查方法是否正好被调用了一次。
BullsEyeMockTests中声明SUT和mock对象:
<pre>var controllerUnderTest: ViewController! var mockUserDefaults: MockUserDefaults!</pre>
在setup()中创建SUT和模拟对象,然后注入模拟对象作为SUT的属性:
<pre>controllerUnderTest = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController! mockUserDefaults = MockUserDefaults(suiteName: "testing")! controllerUnderTest.defaults = mockUserDefaults</pre>
释放SUT和teardown()中的模拟对象:
<pre>controllerundertest = nil mockuserdefaults = nil</pre>
testexample()替换为:
<pre>// Mock to test interaction with UserDefaults func testGameStyleCanBeChanged() { // given let segmentedControl = UISegmentedControl() // when XCTAssertEqual(mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions") segmentedControl.addTarget(controllerUnderTest, action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged) segmentedControl.sendActions(for: .valueChanged) // then XCTAssertEqual(mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed") }</pre>
<p>when断言的条件是在测试方法taps分段控件前gamestylechanged标志为0。所以如果then断言也为真,意味着<code>set(_:forKey:)</code>正好被调用了一次。运行测试,结果应该为成功。</p>

在Xcode中测试用户交互(UI)

<p>在Xcode 7中引入了UI测试,它能让你通过记录你的UI操作来创建UI测试。UI测试通过查找一个应用程序的UI对象进行查询,合成事件,然后将它们发送到这些对象。API使您能够检查UI对象的属性和状态,以便将它们与预期状态进行比较。<p>
打开BullEyes项目,在该项目的测试导航,添加一个新的UI测试目标,然后接受默认名称BullsEyeUITests。
在BullsEyeUITests类顶部添加属性:
<pre><code>var app: XCUIApplication!</code></pre><p>在setup()中将语句<pre><code>XCUIApplication().launch()</code></pre>替换为:</p>
<pre>app = XCUIApplication() app.launch()</pre></p><p>将<code>testExample()</code> 改名为<code>testGameStyleSwitch()</code>。
在<code> testGameStyleSwitch()</code>方法中开始一个新行,然后单击编辑器窗口下方的红色录音按钮:</p>

当应用在模拟器中启动后,点击游戏风格切换开关和对应的顶部标签。然后点击Xcode记录按钮停止录音。
在<code>testGameStyleSwitch()</code>方法中会新生成如下三行:
<pre>```let app = XCUIApplication()
app.buttons["Slide"].tap()
app.staticTexts["Get as close as you can to: "].tap()

如果有其他内容,删除它们。第一行重复了你在<code>setup()</code>中创建的属性,你现在也不需要点击任何东西,所以删除第一行以及第二行和第三行结尾处的<code>.tap()</code>。在代码中打开<code>["Slide"]</code>右侧的小下拉菜单,然后选择<code>segmentedControls.buttons["Slide"]</code>
现在代码的样子会变为:
<pre>```app.segmentedControls.buttons["Slide"]
app.staticTexts["Get as close as you can to: "]```</pre>
修改它以创建given部分:
<pre>```// given
let slideButton = app.segmentedControls.buttons["Slide"]
let typeButton = app.segmentedControls.buttons["Type"]
let slideLabel = app.staticTexts["Get as close as you can to: "]
let typeLabel = app.staticTexts["Guess where the slider is: "]```</pre>现在您等到了两个按钮的名称和两个可能的顶级标签。接着请添加以下内容:
<pre>```// then
if slideButton.isSelected {
   XCTAssertTrue(slideLabel.exists)
   XCTAssertFalse(typeLabel.exists)
   typeButton.tap()
   XCTAssertTrue(typeLabel.exists)
   XCTAssertFalse(slideLabel.exists)
} else if typeButton.isSelected {
   XCTAssertTrue(typeLabel.exists)
   XCTAssertFalse(slideLabel.exists)
   slideButton.tap()
   XCTAssertTrue(slideLabel.exists)
   XCTAssertFalse(typeLabel.exists)
}```</pre>这段代码检查每个按钮被选中时对应的正确的标签是否存在。执行测试-所有的断言应该显示成功。</pre></p>

##性能测试
根据[苹果的文档](https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/04-writing_tests.html#//apple_ref/doc/uid/TP40014132-CH4-SW8):性能测试会先取得你要评估的代码块,然后将它运行十次。收集均执行时间的平均值和标准差。这些单独测量值的平均值形成的结果可以与基准进行比较,以评估测试成功或失败。
<p>写一个性能测试很简单:只需把你想测量到代码放入<code>measure()</code>方法中的闭包中。</p>让我们实际做一下。再次打开HalfTunes项目,在HalfTunesFakeTests中将<code> testPerformanceExample ()</code>替换为:
<pre>```// Performance
func test_StartDownload_Performance() {
   let track = Track(name: "Waterloo", artist: "ABBA",
   previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a")
   measure {
      self.controllerUnderTest?.startDownload(track)
   }
}```</pre>运行测试,然后如下图所示,单击```measure()```方法结尾处的图标来查看统计结果。

![](http://upload-images.jianshu.io/upload_images/4293407-ea824b2e04f1fafe.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

单击“设置基线”,然后再次运行性能测试并查看结果-它可能比基线更好也可能更差。编辑按钮让您可以使用这个新的结果来重置基线。
<p>基线是根据每个设备的配置来存储的,所以你可以将相同的测试在不同的设备上执行,并根据不同的处理器,内存等配置来设定不同的基线。</p>
<p>任何时候做可能会影响正在测试的方法的性能的更改,都请再次运行性能测试,来查看与基线比较,性能的变化。</p>
<h2>代码覆盖</h2><p>代码覆盖工具告诉你哪些应用程序的代码真正被你的测试执行了,这样你就可以知道程序代码的哪些部分还没有被测到。</p>
<pre>注:在启用代码覆盖时,是否应该运行性能测试?[苹果的文档](https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/07-code_coverage.html#//apple_ref/doc/uid/TP40014132-CH15-SW1)说:代码覆盖率数据收集会带来性能上的损失…以线性方式影响代码执行,这样当它启用时,性能测试结果与测试运行到测试运行保持一致。但是,您应该考虑是否在您在测试中严格评估例程的性能时启用代码覆盖率。</pre>
为了启用代码覆盖、编辑方案(scheme)中的“测试动作”的并选中代码覆盖项:(译注:使用 ⌘+<  开始编辑方案) 

![](http://upload-images.jianshu.io/upload_images/4293407-d8b61750f83da482.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

运行所有的测试(⌘+U),然后打开报告导航栏(⌘+8)。按时间选择,选择该列表中的第一项,然后选择“覆盖”选项卡:

![](http://upload-images.jianshu.io/upload_images/4293407-a1fa7d574eae648c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

<p>单击三角形标志可以看到SearchViewController.swift中的函数列表:</p>

![](http://upload-images.jianshu.io/upload_images/4293407-2dfc295ce4dc167b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

将鼠标移动到<code>updateSearchResults(_:)</code>后面的蓝条上可以看到覆盖率为71.88%。
<p>单击此功能的箭头按钮来打开源文件,然后定位该函数。当鼠标越过右侧边栏的覆盖注释时,代码段会高亮显示绿色或红色:</p>
![](http://upload-images.jianshu.io/upload_images/4293407-6bcd8a4527613815.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

覆盖注释显示多少次试验打每个代码段;没有被自行的部分被用红色标出。如你所期望的,```for```循环跑了3次,但出错处理路径上的代码都没有被执行。要增对这些功能的覆盖,你可以复制abbadata.json,然后编辑出不同的错误,例如,将"results"改为“result”,这样可以得到一个能够覆盖<code>print("Results key not found in dictionary")</code>的测试。</p>

###100%覆盖?
<p>应该努力争取达到100%的代码覆盖率么?百度一下“100%单元测试覆盖率”,你会发现一系列的赞成和反对的理由,和对“100%覆盖”的定义的争论。反对方说最后10-15%是不值得努力的。支持方争论说最后的10-15%是最重要的,*因为*它难以测试。百度“hard to unit test bad design"去查找一篇很有说服力的文章[无法验证的代码是一个更深层次的设计问题的标志](https://www.toptal.com/qa/how-to-write-testable-code-and-why-it-matters)。进一步思考很可能会得出这样的结论:正确的方向是[测试驱动开发](http://qualitycoding.org/tdd-sample-archives/)。</p>

##下一步做什么?
<p>现在你已经掌握了一些用于编写项目测试的优秀工具。我希望这个iOS单元测试和UI测试教程给了你信心去测试所有的东西!
您可以在[zip文件](https://koenig-media.raywenderlich.com/uploads/2016/12/Finished-3.zip)中找到已完成的项目。下面是一些深入研究的资源:</p>
<li>现在你已经会为你的项目写测试了。下一步是自动化:持续集成和持续交付。阅读苹果的使用Xcode服务器和xcodebuild[自动化测试过程](https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/08-automation.html#//apple_ref/doc/uid/TP40014132-CH7-SW1),和来自维基百科的[持续交付文章](https://en.wikipedia.org/wiki/Continuous_delivery),这些文章借鉴了[ThoughtWorks](https://www.thoughtworks.com/continuous-delivery)的专家的专业知识。</p>

<li>[在Swift Playgounds中使用TDD](http://initwithstyle.net/2015/11/tdd-in-swift-playgrounds/) 介绍了在Playgounds中使用```XCTestObservationCenter```执行```XCTestCase```单元测试。你可以在Playgounds中开发你的项目代码和编写测试,然后再把两者转移到你的应用中去。
<li>[手表应用:我们如何测试它们?](https://realm.io/news/cmduconf-boris-bugling-how-test-watch-apps/)来自[CMD + U](http://www.cmduconf.com/)会议展示了如何使用[PivotalCoreKit](https://github.com/pivotal/PivotalCoreKit)测试WatchOS应用。
<li>如果你已经有一个应用程序,但还没有为它写过测试,你可能要参考米迦勒的[如何高效地工作在老旧代码上](https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052/ref=sr_1_1?s=books&ie=UTF8&qid=1481511568&sr=1-1),因为没有测试程序的代码就*是*老旧代码!</p><p>
<li>乔恩瑞德的高质量编码示例应用程序文档是一个学习更多的关于[测试驱动开发](http://qualitycoding.org/tdd-sample-archives/)的好地方。
</p><p>如果您对本教程有任何问题或意见,请加入下面的论坛讨论。:]</p>

###团队
www.raywenderlich.com的每个教程都是由我们的专职团队完成,以确保其符合我们的高质量标准。创建本教程的团队成员是:

![](http://upload-images.jianshu.io/upload_images/4293407-47316734f86e712f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

<b>来自译者:</b>如果你认真读到了这里,并按教程完成了里面的示例,我相信你一定和我一样收获良多。测试对我而言一直是深觉重要又觉得无从下手的一个课题,这篇文章虽然没有也无法给出所有我们想要的答案,但无疑打开了一扇大门。<p>如有任何和本文或iOS测试相关的问题,欢迎留言,谢谢。</p>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容