iOS單元測(cè)試和UI測(cè)試全面解析

編寫(xiě)測(cè)試可不是一項(xiàng)迷人的工作;然而,由于測(cè)試可以避免使你的寶貝應(yīng)用程序變成一塊充斥錯(cuò)誤的大垃圾場(chǎng)伦吠,所以編寫(xiě)測(cè)試又是一項(xiàng)非常有必要做的工作鞍历。如果你正在閱讀本文呀打,那么你應(yīng)當(dāng)已經(jīng)知道你應(yīng)該為您的代碼和用戶(hù)界面編寫(xiě)測(cè)試,只是不確定如何在Xcode中編寫(xiě)測(cè)試豌研。
也許你已經(jīng)開(kāi)發(fā)出一個(gè)能夠工作的應(yīng)用程序妹田,只是還沒(méi)有對(duì)它進(jìn)行測(cè)試;另一方面,當(dāng)您擴(kuò)展該應(yīng)用程序時(shí)鹃共,你又想對(duì)其任何的更改進(jìn)行測(cè)試鬼佣。也許你已經(jīng)寫(xiě)了一些測(cè)試,但尚不能確定它們是否是正確的測(cè)試霜浴【е裕或者,你現(xiàn)在正在開(kāi)發(fā)您的應(yīng)用程序阴孟,并且想隨著工作的進(jìn)展對(duì)之進(jìn)行測(cè)試晌纫。
本教程將向您全面展示如何使用Xcode中的測(cè)試導(dǎo)航器來(lái)測(cè)試應(yīng)用程序的模型和異步方法,以及如何通過(guò)使用代理(注stub永丝,有的文章譯作“存根”)和模擬(mock)來(lái)模仿與庫(kù)或系統(tǒng)對(duì)象的交互锹漱,如何測(cè)試用戶(hù)界面和性能,以及如何使用代碼覆蓋工具慕嚷。隨著文章的展開(kāi)哥牍,你會(huì)不斷熟悉一些與測(cè)試相關(guān)的術(shù)語(yǔ),到文章結(jié)尾時(shí)你會(huì)沉著地把依賴(lài)關(guān)系注入到你的被測(cè)系統(tǒng)(SUT喝检,system under test)中!
測(cè)試嗅辣,測(cè)試……
測(cè)試什么?
在寫(xiě)任何測(cè)試之前,首先要明確最基本的問(wèn)題︰你需要測(cè)試什么?如果你的目標(biāo)是擴(kuò)展一款現(xiàn)有的應(yīng)用程序挠说,那么您應(yīng)該首先為您計(jì)劃更改的任何組件編寫(xiě)測(cè)試澡谭。
更一般的情況下,你的測(cè)試應(yīng)包括如下一些內(nèi)容︰
核心功能︰模型類(lèi)和方法及其與控制器的交互
最常見(jiàn)的用戶(hù)界面工作流
邊界條件
錯(cuò)誤修復(fù)
當(dāng)務(wù)之急
首字母縮略詞FIRST描述了一套簡(jiǎn)明有效的單元測(cè)試標(biāo)準(zhǔn)纺涤。這些標(biāo)準(zhǔn)是︰
Fast(快速)︰測(cè)試的運(yùn)行速度應(yīng)該很快译暂,這樣一來(lái)人們就不會(huì)介意運(yùn)行它們抠忘。
Independent/Isolated(獨(dú)立/分離)︰一個(gè)測(cè)試不應(yīng)因另一個(gè)測(cè)試而進(jìn)行安裝或拆卸。
Repeatable(可重復(fù))︰每次運(yùn)行測(cè)試時(shí)外永,您應(yīng)該獲得相同的結(jié)果崎脉。值得注意的是,外部數(shù)據(jù)提供者和并發(fā)問(wèn)題可能會(huì)導(dǎo)致程序的間歇性故障伯顶。
Self-validating(自我驗(yàn)證)︰測(cè)試應(yīng)該能夠完全自動(dòng)化進(jìn)行;輸出應(yīng)該要么是“pass”(即“通過(guò)”)要么是“fail”(即“失敗”)囚灼,而不是提供給程序員一個(gè)解釋性的日志文件。
Timely(及時(shí))︰理想情況下祭衩,應(yīng)該只是在你編寫(xiě)生產(chǎn)代碼之前編寫(xiě)測(cè)試灶体。

遵循上述FIRST原則進(jìn)行測(cè)試能夠確保您的測(cè)試明確而有用,而不致使之成為您的應(yīng)用程序中的路障掐暮。
開(kāi)始
首先蝎抽,請(qǐng)從網(wǎng)址https://koenig-media.raywenderlich.com/uploads/2016/12/Starters.zip處下載、解壓縮路克、打開(kāi)并觀察本文提供的兩個(gè)初始示例工程BullsEye和HalfTunes樟结。
注意,工程BullsEye基于文章https://www.raywenderlich.com/store/ios-apprentice中提供的一個(gè)樣本程序精算。我已經(jīng)把游戲邏輯提取到一個(gè)BullsEyeGame類(lèi)中瓢宦,并相應(yīng)地添加了另一種游戲風(fēng)格。
在游戲的右下角提供了一個(gè)分段的控制器組件灰羽,供用戶(hù)選擇游戲風(fēng)格︰或者是Slide類(lèi)型驮履,允許玩家移動(dòng)滑塊組件以盡可能接近目標(biāo)值;或者是Type類(lèi)型,允許玩家猜測(cè)滑塊到達(dá)的位置廉嚼∶蹈洌控件相應(yīng)的動(dòng)作代碼中還會(huì)將用戶(hù)選擇的游戲風(fēng)格存儲(chǔ)為該用戶(hù)的默認(rèn)設(shè)置。
另一個(gè)示例工程HalfTunes則來(lái)自于我們的另一個(gè)教程N(yùn)SURLSession(https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started)前鹅,現(xiàn)已被更新到Swift 3版本摘悴。用戶(hù)可以使用iTunes API查詢(xún)歌曲,然后下載并播放對(duì)應(yīng)的歌曲片段舰绘。
下面蹂喻,讓我們正式開(kāi)始測(cè)試!
Xcode中的單元測(cè)試
創(chuàng)建單元測(cè)試目標(biāo)
Xcode中的測(cè)試導(dǎo)航器(Test Navigator)為進(jìn)行程序測(cè)試提供了最容易使用的方式;你可以使用它創(chuàng)建測(cè)試目標(biāo)并在你的程序上運(yùn)行測(cè)試。
現(xiàn)在捂寿,請(qǐng)打開(kāi)工程BullsEye并按下組合鍵Command+5來(lái)打開(kāi)它的測(cè)試導(dǎo)航器口四。
然后,點(diǎn)擊左下方的+按鈕;之后秦陋,從菜單中選擇“New Unit Test Target…”命令蔓彩,如圖所示。


在此,請(qǐng)直接使用默認(rèn)的名稱(chēng)BullsEyeTests赤嚼。當(dāng)測(cè)試包出現(xiàn)在測(cè)試導(dǎo)航器中時(shí)旷赖,單擊它,從而在編輯器中打開(kāi)它更卒。如果BullsEyeTests不會(huì)自動(dòng)出現(xiàn)等孵,你可以單擊其他導(dǎo)航器,然后再返回到當(dāng)前測(cè)試導(dǎo)航器即可蹂空。



注意到俯萌,模板導(dǎo)入了XCTest并定義了XCTestCase的一個(gè)子類(lèi)BullsEyeTests,同時(shí)提供了setup()方法上枕,tearDown()方法咐熙,還有系統(tǒng)默認(rèn)的示例測(cè)試方法。
歸納起來(lái)辨萍,共有三種辦法可以運(yùn)行測(cè)試類(lèi):

  1. 使用命令Product\Test或者Command-U;這將會(huì)運(yùn)行所有的測(cè)試類(lèi)棋恼。
  2. 使用測(cè)試導(dǎo)航器中的箭頭命令。
  3. 也可以點(diǎn)擊代碼左邊緣上的鉆石按鈕锈玉。

另外蘸泻,您還可以通過(guò)單擊測(cè)試導(dǎo)航器中或代碼左邊緣上的鉆石按鈕運(yùn)行單個(gè)測(cè)試方法。
建議你嘗試上面不同的方式來(lái)運(yùn)行測(cè)試嘲玫,從而感受一下需要多長(zhǎng)時(shí)間以及運(yùn)行測(cè)試看起來(lái)的樣子。當(dāng)前的樣本測(cè)試并不做任何事并扇,所以它們的運(yùn)行速度會(huì)非橙ネ牛快!
當(dāng)所有測(cè)試都成功時(shí),鉆石按鈕會(huì)變綠穷蛹,并在上面顯示對(duì)號(hào)標(biāo)記土陪。你可以單擊testPerformanceExample()方法最后面的灰色鉆石按鈕來(lái)打開(kāi)性能結(jié)果(Performance Result)小窗進(jìn)行觀察,參考下圖肴熏。


現(xiàn)在鬼雀,我們并不需要函數(shù)testPerformanceExample();所以,把它刪除即可蛙吏。
使用XCTAssert測(cè)試模型
首先源哩,您將使用XCTAssert來(lái)測(cè)試BullsEye模型的一個(gè)核心功能︰一個(gè)BullsEyeGame對(duì)象能否正確計(jì)算出一個(gè)回合的得分?
為此,請(qǐng)?jiān)谖募﨎ullsEyeTests.swift中緊貼著導(dǎo)入語(yǔ)句下方添加下面這一行代碼︰

@testable import BullsEye 

這一行代碼使單元測(cè)試能夠訪問(wèn)到BullsEye中的類(lèi)和方法鸦做。
接下來(lái)励烦,請(qǐng)?jiān)贐ullsEyeTests類(lèi)的頂部添加下面的屬性:

var gameUnderTest: BullsEyeGame! 

然后,在setup()方法中在調(diào)用超類(lèi)語(yǔ)句的下面啟動(dòng)一個(gè)新的BullsEyeGame對(duì)象:

gameUnderTest = BullsEyeGame() 
 
gameUnderTest.startNewGame() 

上面的代碼將創(chuàng)建一個(gè)類(lèi)級(jí)的SUT(System Under Test泼诱,測(cè)試系統(tǒng))對(duì)象坛掠。這樣一來(lái),測(cè)試類(lèi)中的所有測(cè)試都可以訪問(wèn)該SUT對(duì)象的屬性和方法。
在這里屉栓,你還可以調(diào)用游戲的startNewGame方法——此方法只創(chuàng)建一個(gè)targetValue值舷蒲。您的很多測(cè)試都將使用這個(gè)targetValue值,來(lái)測(cè)試程序能夠正確計(jì)算出游戲中的得分友多。
最后牲平,切記在tearDown()方法中在調(diào)用超類(lèi)前釋放掉你的SUT對(duì)象︰

gameUnderTest = nil 

【注意】一種值得推薦的測(cè)試做法是在方法setup()中創(chuàng)建SUT對(duì)象并在tearDown()方法中釋放它,以確保每個(gè)測(cè)試都對(duì)應(yīng)一個(gè)徹底的清理夷陋。更多的有關(guān)細(xì)節(jié)討論欠拾,請(qǐng)參考Jon Reid的帖子http://qualitycoding.org/teardown/
現(xiàn)在骗绕,你已經(jīng)準(zhǔn)備好編寫(xiě)你的第一個(gè)測(cè)試了!
請(qǐng)使用如下代碼替換工程中的方法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") 
} 

測(cè)試方法的名稱(chēng)總是以test開(kāi)頭藐窄,后面跟著的是對(duì)它要測(cè)試的內(nèi)容的說(shuō)明。
一個(gè)推薦的做法是把測(cè)試方法格式化成given酬土、when和then等幾部分︰

  1. 在given部分中荆忍,設(shè)置所需的任何值。在此示例中撤缴,您創(chuàng)建一個(gè)猜測(cè)值刹枉,以便可以指定它與targetValue值區(qū)別多大。
  2. 在when部分中屈呕,執(zhí)行被測(cè)試代碼——調(diào)用方法gameUnderTest.check(_:)微宝。
  3. 在then部分中,斷言你期望的結(jié)果(在現(xiàn)在情況下虎眨,gameUnderTest.scoreRound的值是100-5):如果測(cè)試失敗則打印對(duì)應(yīng)的消息蟋软。

現(xiàn)在,你可以單擊測(cè)試導(dǎo)航器或者代碼左邊的鉆石圖標(biāo)按鈕運(yùn)行測(cè)試嗽桩。你會(huì)注意到應(yīng)用程序?qū)⑦M(jìn)行構(gòu)建并運(yùn)行起來(lái)岳守,最后鉆石圖標(biāo)將更改為一個(gè)綠色的對(duì)號(hào)標(biāo)記!
【注意】若要查看XCTestAssertions的完整列表,你可以在按下Command鍵的同時(shí)單擊代碼中的XCTAssertEqual打開(kāi)文件XCTestAssertions.h碌冶。此外湿痢,你還可以參考蘋(píng)果官方網(wǎng)站提供的按類(lèi)別提供的斷言列表
(https://developer.apple.com/library/prerelease/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/04-writing_tests.html#//apple_ref/doc/uid/TP40014132-CH4-SW35)。
另外扑庞,上述測(cè)試中的Given-When-Then結(jié)構(gòu)來(lái)源于行為驅(qū)動(dòng)測(cè)試(Behavior Driven Development譬重,簡(jiǎn)稱(chēng)BDD)中的易于理解的行業(yè)術(shù)語(yǔ)。其實(shí)罐氨,你還可以使用另外一些命名系統(tǒng)害幅,例如Arrange-Act-Assert和Assemble-Activate-Assert,等等岂昭。
調(diào)試一個(gè)測(cè)試
在BullsEyeGame工程中以现,我故意放置了一個(gè)錯(cuò)誤『菰梗現(xiàn)在,我們進(jìn)行測(cè)試邑遏,以便找到這個(gè)錯(cuò)誤佣赖。為了觀察此錯(cuò)誤導(dǎo)致的問(wèn)題,請(qǐng)把testScoreIsComputed重新命名為testScoreIsComputedWhenGuessGTTarget记盒,然后復(fù)制憎蛤、粘貼并編輯它,從而創(chuàng)建另一個(gè)方法testScoreIsComputedWhenGuessLTTarget纪吮。
在該測(cè)試中俩檬,在given部分把targetValue減去5,其他保持不變碾盟。詳見(jiàn)下列代碼:

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") 
} 

注意到:猜測(cè)值和targetValue值之間的區(qū)別仍然是5棚辽,因此分?jǐn)?shù)應(yīng)仍為95。
在斷點(diǎn)導(dǎo)航器中冰肴,添加一個(gè)測(cè)試失敗(Test Failure)斷點(diǎn);當(dāng)一個(gè)測(cè)試方法發(fā)出一個(gè)失敗的斷言時(shí)這將停止測(cè)試運(yùn)行屈藐。


現(xiàn)在運(yùn)行你的測(cè)試:它應(yīng)該在XCTAssertEqual一行停止,并出示一個(gè)測(cè)試錯(cuò)誤熙尉。
然后联逻,你可以在調(diào)試控制臺(tái)上觀察gameUnderTest和guess的輸出結(jié)果:


你應(yīng)該注意到:guess的值是-5,但scoreRound的值是105检痰,而不是95!
為了進(jìn)一步分析包归,你可以使用通常的調(diào)試過(guò)程︰在when語(yǔ)句上設(shè)置一個(gè)斷點(diǎn),也在BullsEyeGame.swift文件上設(shè)置一個(gè)斷點(diǎn)——即在其中的方法check(:)上設(shè)置铅歼。然后箫踩,再次運(yùn)行測(cè)試,并以逐過(guò)程調(diào)試方式(即step-over)調(diào)試let語(yǔ)句來(lái)檢查應(yīng)用程序中的不同值

現(xiàn)在的問(wèn)題是谭贪,差值是一個(gè)負(fù)數(shù);所以耳高,得分是100-(-5)排抬。解決方法是使用差異的絕對(duì)值即可静暂。為此痴鳄,在方法check(
:)中取消正確代碼前面的注釋?zhuān)h除不正確的代碼即可家浇。
刪除上面設(shè)置的兩個(gè)斷點(diǎn)并再一次運(yùn)行測(cè)試挫望,以確認(rèn)上面代碼行現(xiàn)在已順利通過(guò)驾锰。
使用XCTestExpectation測(cè)試異步操作
到目前為止扮碧,你已經(jīng)學(xué)會(huì)了如何測(cè)試模型和調(diào)試測(cè)試失敗磁椒。接下來(lái)堤瘤,讓我們繼續(xù)學(xué)習(xí)如何使用XCTestExpectation來(lái)測(cè)試網(wǎng)絡(luò)相關(guān)的操作。
首先浆熔,請(qǐng)打開(kāi)HalfTunes項(xiàng)目本辐。你會(huì)注意到,它使用URLSession來(lái)查詢(xún)iTunes API和下載歌曲樣本。假設(shè)您想修改它慎皱,以便使用AlamoFire進(jìn)行網(wǎng)絡(luò)操作老虫。為了查看是否出現(xiàn)任何中斷情況,您應(yīng)為網(wǎng)絡(luò)操作編寫(xiě)測(cè)試茫多,并在更改代碼之前和之后運(yùn)行它們祈匙。
URLSession方法是異步執(zhí)行的︰它們會(huì)馬上返回,但只有運(yùn)行一段時(shí)間后才真正完成天揖。為了測(cè)試異步方法夺欲,你應(yīng)使用XCTestExpectation使你的測(cè)試等待異步操作完成。
值得注意的是今膊,異步測(cè)試通常很慢些阅,所以你應(yīng)該把它們與你另外的一些運(yùn)行速度更快的單元測(cè)試分開(kāi)。
從菜單“+”下選擇并運(yùn)行命令“New Unit Test Target…”万细,然后把目標(biāo)命名為HalfTunesSlowTests扑眉。然后,在import語(yǔ)句的下面導(dǎo)入HalfTunes程序:

@testable import HalfTunes 

在此類(lèi)中的所有測(cè)試都將使用默認(rèn)會(huì)話(huà)把請(qǐng)求發(fā)送到蘋(píng)果公司的服務(wù)器赖钞。所以腰素,我們?cè)诜椒╯etup()中聲明并創(chuàng)建一個(gè)sessionUnderTest對(duì)象,然后在方法tearDown()中釋放它:

var sessionUnderTest: URLSession! 
override func setUp() { 
  super.setUp() 
  sessionUnderTest = URLSession(configuration: URLSessionConfiguration.default) 
} 
override func tearDown() { 
  sessionUnderTest = nil 
  super.tearDown() 
} 

接下來(lái)雪营,使用TestExample()函數(shù)來(lái)替換您的異步測(cè)試︰

//異步測(cè)試時(shí):成功測(cè)試很快弓千,失敗測(cè)試卻比較慢 
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) 
} 

上面這個(gè)測(cè)試的目的是檢查發(fā)送到iTunes的有效的查詢(xún)是否能夠返回狀態(tài)碼200。顯然献起,其中大部分代碼與你在上面應(yīng)用程序中所寫(xiě)的一樣洋访,只是增加了如下幾行︰

1.expectation(:)返回一個(gè)XCTestExpectation對(duì)象;此對(duì)象存儲(chǔ)在變量promise中。此對(duì)象的其他常用名字是expectation和future谴餐。另外姻政,description參數(shù)描述了你期望發(fā)生的事情。
2.為了匹配description參數(shù)岂嗓,您需要在異步方法的完成處理程序的成功條件閉包中調(diào)用promise.fulfill()汁展。
3.waitForExpectations(
:handler:)的作用是保持所有測(cè)試在運(yùn)行中,直到所有的期望得以實(shí)現(xiàn)厌殉,或者timeout值指定的時(shí)間間隔結(jié)束——無(wú)論兩者哪一種早發(fā)生都行食绿。
現(xiàn)在,再來(lái)運(yùn)行該測(cè)試公罕。如果你已經(jīng)連接到互聯(lián)網(wǎng)器紧,則當(dāng)應(yīng)用程序在模擬器中加載后成功測(cè)試大約花費(fèi)一秒鐘時(shí)間。
使測(cè)試失敗更快一些
測(cè)試失敗會(huì)導(dǎo)致不少問(wèn)題楼眷,但它未必花費(fèi)很多時(shí)間〔簦現(xiàn)在熊尉,我們來(lái)解決如何快速確定是否您的測(cè)試失敗的問(wèn)題。
為了修改一下您的測(cè)試桥状,從而導(dǎo)致異步操作時(shí)失敗帽揪,你只需要從下面的URL中刪除“itunes”一詞后面的s字母即可:

let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba") 

運(yùn)行上述測(cè)試時(shí)︰它會(huì)失敗,而且此測(cè)試會(huì)花費(fèi)所有指定的超時(shí)間隔時(shí)間!這是因?yàn)樗钠谕钦?qǐng)求成功——正是在這個(gè)位置調(diào)用了promise.fulfill()方法辅斟。既然請(qǐng)求失敗转晰,那么測(cè)試僅當(dāng)在超過(guò)指定時(shí)限時(shí)才結(jié)束。
你可以使這個(gè)測(cè)試失敗更快一些——這只要通過(guò)改變它的期望值即可達(dá)到︰不是等待請(qǐng)求成功士飒,而只需要等到異步方法的完成處理程序觸發(fā)即可查邢。只要應(yīng)用程序接收到來(lái)自服務(wù)器端的響應(yīng)(或者是成功或者是失敗)這種情況就會(huì)發(fā)生;但是,這的確符合預(yù)期結(jié)果酵幕。然后扰藕,您的測(cè)試可以檢查請(qǐng)求是否成功。
為了查看這是如何工作的芳撒,您要?jiǎng)?chuàng)建一個(gè)新的測(cè)試邓深。首先,修復(fù)此測(cè)試——這可以通過(guò)撤消上面的url更改操作輕松完成笔刹,然后將下面的測(cè)試添加到您的類(lèi)中︰

// 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) 
} 

上面代碼中最關(guān)鍵的一點(diǎn)是芥备,只需輸入完成處理程序?qū)崿F(xiàn)的期望——這需要大約一秒鐘即會(huì)發(fā)生。如果請(qǐng)求失敗舌菜,那么斷言也會(huì)失敗萌壳。
現(xiàn)在再來(lái)運(yùn)行上面的測(cè)試︰它現(xiàn)在大約需要一秒鐘即會(huì)失敗;它的失敗是因?yàn)檎?qǐng)求失敗了,而不是因?yàn)闇y(cè)試運(yùn)行超時(shí)日月。
修復(fù)上面的url袱瓮,然后再一次運(yùn)行測(cè)試,以確認(rèn)它現(xiàn)在能夠成功通過(guò)測(cè)試爱咬。
偽造對(duì)象和交互
異步測(cè)試能夠給你信心——你的代碼會(huì)為一個(gè)異步API提供正確的輸入尺借。你可能也想測(cè)試您的代碼能夠正常工作——當(dāng)它從URLSession接收輸入時(shí),或當(dāng)它正確更新了UserDefaults或者CloudKit數(shù)據(jù)庫(kù)時(shí)精拟。
大多數(shù)應(yīng)用程序都會(huì)與系統(tǒng)或庫(kù)對(duì)象(你不能控制這些對(duì)象)進(jìn)行交互燎斩,而與這些對(duì)象的交互測(cè)試很可能是極其緩慢的,而且不可重復(fù)的——這正違反了文章開(kāi)始時(shí)FIRST原則中的兩條串前。相反,你可以偽造這些交互——通過(guò)從代理(stub)中獲取輸入或更新模擬對(duì)象(Mock Object)來(lái)實(shí)現(xiàn)实蔽。
當(dāng)您的代碼依賴(lài)于一個(gè)系統(tǒng)或庫(kù)中的對(duì)象時(shí)荡碾,通過(guò)上面?zhèn)卧斓霓k法可以創(chuàng)建一個(gè)假的對(duì)象來(lái)實(shí)現(xiàn)那一部分功能并把這種偽造注入到您的代碼中。喬恩·里德的依賴(lài)性注入技術(shù)文章(https://www.objc.io/issues/15-testing/dependency-injection/)中就介紹了好幾種方法來(lái)達(dá)到這一目的局装。


從代理(stub)中偽造輸入
在本節(jié)中的測(cè)試中坛吁,你將要檢查應(yīng)用程序的updateSearchResults(_:)方法能夠正確解析由會(huì)話(huà)下載的數(shù)據(jù)——通過(guò)檢查屬性searchResults.count的值是正確的來(lái)實(shí)現(xiàn)劳殖。SUT是視圖控制器;你要使用代理(stub)技術(shù)來(lái)偽裝一個(gè)會(huì)話(huà)和一些預(yù)先下載的數(shù)據(jù)。
為此拨脉,從“+”菜單下選擇命令“New Unit Test Target…”并命名它為HalfTunesFakeTests哆姻。然后,在import語(yǔ)句的下面導(dǎo)入HalfTunes程序:

@testable import HalfTunes 

接下來(lái)玫膀,聲明SUT矛缨,并在setup()方法中創(chuàng)建它,且在tearDown()方法中對(duì)之進(jìn)行釋放:

var controllerUnderTest: SearchViewController! 
  
override func setUp() { 
  super.setUp() 
  controllerUnderTest = UIStoryboard(name: "Main",  
      bundle: nil).instantiateInitialViewController() as! SearchViewController! 
} 
  
override func tearDown() { 
  controllerUnderTest = nil 
  super.tearDown() 
} 

【注】SUT(被測(cè)系統(tǒng))是視圖控制器帖旨,因?yàn)镠alfTunes工程中擁有大量的視圖控制器問(wèn)題——所有的工作都是在文件searchviewcontroller.swift中完成的箕昭。“將網(wǎng)絡(luò)代碼移動(dòng)到單獨(dú)的模塊”(詳見(jiàn)文章http://williamboles.me/networking-with-nsoperation-as-your-wingman/)將會(huì)減少這一問(wèn)題解阅,而且也使測(cè)試更為容易落竹。
接下來(lái),您將需要一些樣本JSON數(shù)據(jù)货抄,供您的偽造的會(huì)話(huà)提供給你的測(cè)試使用述召。只需要做一少部分工作即可;因此,請(qǐng)限制一下您的來(lái)自iTunes的下載結(jié)果——在URL字符串的后面添加一個(gè)限制串&limit=3:
https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3
復(fù)制此URL并把它粘貼到瀏覽器中蟹地。這將下載一個(gè)名為1.txt或類(lèi)似的文件积暖。你可以預(yù)覽一下它,以便確認(rèn)這是一個(gè)JSON格式的文件锈津,然后重命名它為abbaData.json呀酸,并把該文件添加到HalfTunesFakeTests組中。
HalfTunes項(xiàng)目包含了支持文件DHURLSessionMock.swift琼梆。這個(gè)文件中定義了一個(gè)簡(jiǎn)單的協(xié)議——DHURLSession性誉,其提供的方法(代理)用于使用一個(gè)URL或URLRequest來(lái)創(chuàng)建一個(gè)數(shù)據(jù)任務(wù)。它還定義了符合該協(xié)議的URLSessionMock對(duì)象茎杂,該對(duì)象中提供的初始化器可以讓你使用你選擇的數(shù)據(jù)错览、響應(yīng)和誤差等來(lái)創(chuàng)造一個(gè)模擬URLSession對(duì)象。
現(xiàn)在煌往,我們來(lái)構(gòu)建偽造的數(shù)據(jù)和響應(yīng)倾哺,并創(chuàng)建偽造的會(huì)話(huà)對(duì)象;這些都實(shí)現(xiàn)于方法setup()中,相應(yīng)的代碼位于創(chuàng)建SUT對(duì)象的語(yǔ)句之后:

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) 
At the end of setup(), inject the fake session into the app as a property of the SUT: 
 
controllerUnderTest.defaultSession = sessionMock 

【注意】您將直接在您的測(cè)試中使用偽造的會(huì)話(huà)刽脖,但是這將向你展示如何注入這種偽造的會(huì)話(huà);這樣一來(lái)羞海,你進(jìn)一步的測(cè)試可以調(diào)用使用視圖控制器defaultSession屬性的SUT方法。
現(xiàn)在曲管,您可以編寫(xiě)測(cè)試來(lái)檢查是否調(diào)用updateSearchResults(_:)方法能夠解析偽造的數(shù)據(jù)却邓。為此,請(qǐng)把TestExample()方法替換為以下內(nèi)容︰

//使用DHURLSession協(xié)議和代理偽造URLSession 
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") 
} 

注意院水,你仍然要以異步方式來(lái)編寫(xiě)這個(gè)測(cè)試腊徙,因?yàn)榇?stub)假裝自己是一個(gè)異步的方法简十。
上面代碼中,when斷言的作用是:在數(shù)據(jù)任務(wù)運(yùn)行之前searchResults的值應(yīng)當(dāng)是空的——這應(yīng)該是真實(shí)情況撬腾,因?yàn)槟趕etup()方法中創(chuàng)建了一個(gè)全新的SUT螟蝙。
偽造的數(shù)據(jù)包含了提供給三個(gè)跟蹤(Track)對(duì)象使用的JSON數(shù)據(jù);所以,then斷言的作用是:視圖控制器的searchResults數(shù)組應(yīng)當(dāng)包含三項(xiàng)民傻。
再次運(yùn)行該測(cè)試胰默。這次應(yīng)該成功,而且速度很快饰潜,因?yàn)椴淮嬖谌魏握鎸?shí)的網(wǎng)絡(luò)連接!
偽造對(duì)模擬對(duì)象的更新
以前的測(cè)試使用代理從假對(duì)象提供輸入初坠。接下來(lái),你可以使用一個(gè)模擬對(duì)象來(lái)測(cè)試你的代碼可以正確更新UserDefaults彭雾。
重新打開(kāi)BullsEye項(xiàng)目碟刺。注意到,該應(yīng)用程序提供了兩種游戲風(fēng)格:用戶(hù)可以選擇移動(dòng)滑塊來(lái)匹配目標(biāo)值或從滑塊位置猜測(cè)目標(biāo)值薯酝。借助于界面右下角的分段控制開(kāi)關(guān)可以切換游戲風(fēng)格并更新用戶(hù)默認(rèn)的游戲風(fēng)格半沽。
你要編寫(xiě)的下一個(gè)測(cè)試將檢查應(yīng)用程序能夠正確地更新用戶(hù)默認(rèn)的游戲風(fēng)格數(shù)據(jù)。
在測(cè)試導(dǎo)航器中吴菠,點(diǎn)擊命令“New Unit Test Target…”者填,并命名為BullsEyeMockTests。然后做葵,在導(dǎo)入語(yǔ)句下面添加以下內(nèi)容:

@testable import BullsEye 
  
class MockUserDefaults: UserDefaults { 
  var gameStyleChanged = 0 
  override func set(_ value: Int, forKey defaultName: String) { 
    if defaultName == "gameStyle" { 
      gameStyleChanged += 1 
    } 
  } 
} 

注意到占哟,上面的MockUserDefaults類(lèi)重載了set(_:forKey:)方法以便把gameStyleChanged標(biāo)志的值加1。通常你會(huì)看到類(lèi)似的測(cè)試中是設(shè)置一個(gè)布爾變量酿矢,但是在此我們使用一個(gè)整數(shù)值加1榨乎,這可以進(jìn)一步增加你的靈活控制——例如你的測(cè)試可以檢查該方法僅被正確地調(diào)用一次。
在BullsEyeMockTests類(lèi)中聲明SUT對(duì)象和模擬對(duì)象:

var controllerUnderTest: ViewController! 
var mockUserDefaults: MockUserDefaults! 

在方法setup()中瘫筐,創(chuàng)建SUT對(duì)象和模擬對(duì)象蜜暑,然后把此模擬對(duì)象注入為該SUT的一個(gè)屬性:

controllerUnderTest = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController! 
mockUserDefaults = MockUserDefaults(suiteName: "testing")! 
controllerUnderTest.defaults = mockUserDefaults 
Release the SUT and the mock object in tearDown(): 
controllerUnderTest = nil 
mockUserDefaults = nil 
Replace testExample() with this: 
// 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") 
} 

上述代碼中的when斷言的作用是:gameStyleChanged標(biāo)志的值為0——在測(cè)試方法觸發(fā)分段控制開(kāi)關(guān)之前。因此策肝,如果then斷言也為真肛捍,那么將意味著方法set(_:forKey:)僅被正確地調(diào)用一次。
現(xiàn)在再次運(yùn)行測(cè)試;應(yīng)當(dāng)可以成功之众。
在Xcode中進(jìn)行UI測(cè)試
Xcode 7中引入了對(duì)UI測(cè)試的支持拙毫,使您可以通過(guò)記錄與UI的交互來(lái)創(chuàng)建UI測(cè)試。UI測(cè)試的工作方式是:通過(guò)查詢(xún)來(lái)查找一個(gè)應(yīng)用程序的UI對(duì)象棺禾,進(jìn)而合成事件缀蹄,然后將這些事件發(fā)送給這些對(duì)象。其提供的API使您可以檢查一個(gè)用戶(hù)界面對(duì)象的屬性和狀態(tài),以便把它們與預(yù)期的狀態(tài)進(jìn)行比較袍患。
現(xiàn)在,讓我們?cè)贐ullsEye項(xiàng)目的測(cè)試導(dǎo)航器中添加一個(gè)新的UI測(cè)試目標(biāo)竣付。確保要被測(cè)試的目標(biāo)是BullsEye诡延,然后接受默認(rèn)名稱(chēng)BullsEyeUITests。
然后古胆,在BullsEyeUITests類(lèi)的頂部添加如下屬性︰

var app: XCUIApplication! 

在方法setup()中肆良,用以下代碼替換XCUIApplication().launch()語(yǔ)句︰

app = XCUIApplication() 
 
app.launch() 

把testExample()的名字更改為testGameStyleSwitch()。
然后逸绎,在testGameStyleSwitch()中按下回車(chē)鍵創(chuàng)建一個(gè)新的空行惹恃,并點(diǎn)擊編輯器窗口底部的紅色的Record按鈕,如圖所示棺牧。


當(dāng)應(yīng)用程序出現(xiàn)在模擬器中時(shí)巫糙,點(diǎn)擊控制游戲風(fēng)格開(kāi)關(guān)的滑動(dòng)塊及頂部標(biāo)簽。然后颊乘,單擊Xcode中的Record按鈕即可停止錄制参淹。
現(xiàn)在,你在方法testGameStyleSwitch()中擁有以下三行代碼︰

let app = XCUIApplication() 
 
app.buttons["Slide"].tap() 
 
app.staticTexts["Get as close as you can to: "].tap() 

如果還有其他的語(yǔ)句乏悄,則刪除它們浙值。
第一行代碼的作用是復(fù)制你在setup()方法中創(chuàng)建的屬性;因?yàn)槟氵€不需要點(diǎn)擊任何東西,所以也把這第一行刪除檩小,還要?jiǎng)h除第2行與第3行末尾的“.tap()”开呐。打開(kāi)["Slide"]鄰近的小菜單并選擇
segmentedControls.buttons["Slide"]。
于是规求,你有了如下的代碼:

app.segmentedControls.buttons["Slide"] 
 
app.staticTexts["Get as close as you can to: "] 

進(jìn)一步修改上述代碼筐付,以便創(chuàng)建測(cè)試的given部分:

// 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: "] 

現(xiàn)在,你有了兩個(gè)按鈕和兩個(gè)可能的頂部標(biāo)簽的名稱(chēng)颓哮,再添加以下內(nèi)容︰

// 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) 
 
} 

這段代碼將會(huì)檢測(cè)當(dāng)選中或者點(diǎn)擊每個(gè)按鈕時(shí)是否存在正確的標(biāo)簽〖易保現(xiàn)在,運(yùn)行測(cè)試——結(jié)果是所有斷言應(yīng)該都成功冕茅。
性能測(cè)試
根據(jù)蘋(píng)果公司官方文檔
(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)描述:一個(gè)性能測(cè)試需要使用你想要評(píng)估的一個(gè)代碼塊伤极,并運(yùn)行此代碼塊10次,期間收集平均執(zhí)行時(shí)間和運(yùn)行的標(biāo)準(zhǔn)偏差值姨伤。這些個(gè)別測(cè)量的平均值成為測(cè)試運(yùn)行的一個(gè)值哨坪,然后把該值與一個(gè)基準(zhǔn)值進(jìn)行比較來(lái)評(píng)估成功或失敗。
寫(xiě)一個(gè)性能測(cè)試還是非常簡(jiǎn)單的︰你只需要把你想要測(cè)試的代碼放到measure()方法的閉包中即可乍楚。
為了實(shí)際體驗(yàn)一下当编,請(qǐng)重新打開(kāi)HalfTunes項(xiàng)目,然后在HalfTunesFakeTests類(lèi)中使用下面的測(cè)試徒溪,從而替換掉系統(tǒng)默認(rèn)生成的testPerformanceExample()方法︰

// 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) 
  } 
} 

現(xiàn)在忿偷,請(qǐng)運(yùn)行上面的測(cè)試金顿,然后單擊measure()閉包末尾的圖標(biāo)來(lái)觀看統(tǒng)計(jì)信息。


單擊“Set Baseline”(設(shè)置基準(zhǔn)值)按鈕鲤桥,然后再次運(yùn)行性能測(cè)試并查看結(jié)果——結(jié)果有可能比基準(zhǔn)值更好或更糟揍拆。你可以點(diǎn)擊Edit(編輯)按鈕幫助您將基準(zhǔn)值重置為這個(gè)新的結(jié)果。
基準(zhǔn)值在每個(gè)設(shè)備配置時(shí)存儲(chǔ)起來(lái)茶凳,所以你可以讓同一測(cè)試執(zhí)行在若干臺(tái)不同的設(shè)備上嫂拴,并使每臺(tái)設(shè)備保持一個(gè)不同的基準(zhǔn)值——這要取決于處理器速度、內(nèi)存等的具體配置情況贮喧。
任何時(shí)候只要你更改一個(gè)應(yīng)用程序筒狠,都有可能影響正在測(cè)試的方法的性能;你可以再次運(yùn)行性能測(cè)試來(lái)觀察當(dāng)前值與基準(zhǔn)值比較的結(jié)果。
代碼覆蓋
代碼覆蓋工具能夠告訴你應(yīng)用程序中的哪些代碼實(shí)際上被您的測(cè)試運(yùn)行過(guò);這樣一來(lái)箱沦,你就可以知道應(yīng)用程序代碼的哪些部分還沒(méi)有被測(cè)試辩恼。
【注意】在啟用代碼覆蓋功能時(shí)你是否應(yīng)該運(yùn)行性能測(cè)試呢?蘋(píng)果公司的文檔(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)中是這樣描述的︰代碼覆蓋數(shù)據(jù)集合會(huì)導(dǎo)致性能的下降……以線(xiàn)性方式影響代碼的執(zhí)行;因此,當(dāng)啟用代碼覆蓋功能時(shí)程序的性能將會(huì)因不同的測(cè)試運(yùn)行而有所差異谓形。但是运挫,當(dāng)你對(duì)你的測(cè)試中的例程要求極其嚴(yán)格時(shí)你應(yīng)該認(rèn)真考慮是否要啟用代碼覆蓋支持。
為了啟用代碼覆蓋功能套耕,你可以編輯一下你預(yù)先計(jì)劃的測(cè)試(Test)操作并勾選“Code Coverage”復(fù)選框︰

運(yùn)行您的所有測(cè)試(按下組合鍵Command+U)谁帕,然后打開(kāi)報(bào)告導(dǎo)航器(按下組合鍵Command+8)。按執(zhí)行時(shí)間先后選擇(By Time冯袍,見(jiàn)下圖)列表中最上面的一項(xiàng)匈挖,然后再選擇“Coverage”(覆蓋)選項(xiàng)卡。

你可以單擊如下圖展開(kāi)的三角形圖標(biāo)來(lái)觀察SearchViewController.swift文件中的函數(shù)列表︰

你可以把鼠標(biāo)懸停在updateSearchResults(_:)方法附近的藍(lán)色的Coverage(覆蓋率)條上觀察到對(duì)應(yīng)的覆蓋率為71.88%康愤。
單擊該函數(shù)對(duì)應(yīng)的箭頭按鈕來(lái)打開(kāi)源文件儡循,并定位到該函數(shù)。當(dāng)你的鼠標(biāo)移到右邊欄中的覆蓋率注釋上時(shí)征冷,代碼段將突出顯示為綠色或紅色︰

覆蓋率注釋上的信息顯示出一個(gè)測(cè)試中命中每個(gè)代碼段的次數(shù)择膝。注意,沒(méi)有被調(diào)用到的代碼段部分突出顯示為紅色检激。正如你所期望的肴捉,for循環(huán)運(yùn)行3次,但沒(méi)有一次是沿著錯(cuò)誤路徑執(zhí)行的叔收。為了提高此函數(shù)的代碼覆蓋率齿穗,你可以復(fù)制abbaData.json,然后修改它饺律,使其會(huì)導(dǎo)致不同的錯(cuò)誤——例如窃页,將“results”更改為“result”來(lái)測(cè)試執(zhí)行到打印語(yǔ)句print("Results key not found in dictionary")的情況。
100%覆蓋?
爭(zhēng)取實(shí)現(xiàn)100%的代碼覆蓋率你可知道應(yīng)該付出怎樣的努力嗎?如果你使用谷歌搜索引擎搜索“100% unit test coverage”的話(huà),你會(huì)搜索到有贊同的也有反對(duì)的等多種觀點(diǎn)脖卖,以及圍繞100%覆蓋率的大量爭(zhēng)論乒省。其中,持反對(duì)看法的認(rèn)為最后的10-15%并不重要——不值得為之付出努力;而持贊同看法的認(rèn)為最后的10-15%極其重要——因?yàn)樗茈y測(cè)試畦木。再使用谷歌搜索引擎搜索“hard to unit test bad design”可以找到頗有說(shuō)服力的論據(jù)——無(wú)法驗(yàn)證的代碼是一種更深層次的設(shè)計(jì)問(wèn)題(https://www.toptal.com/qa/how-to-write-testable-code-and-why-it-matters)作儿。進(jìn)一步的思考可能導(dǎo)致的結(jié)論是測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(http://qualitycoding.org/tdd-sample-archives/)是軟件開(kāi)發(fā)過(guò)程中必須要走的路。
總結(jié)
本文中已經(jīng)向你提供了為你的iOS工程編寫(xiě)測(cè)試的多種工具馋劈。我希望你能夠通過(guò)本教程的學(xué)習(xí)樹(shù)立起足夠的信心來(lái)測(cè)試一切!
你可以從地址https://koenig-media.raywenderlich.com/uploads/2016/12/Finished-3.zip處下載本文中的完整的示例工程源碼。
最后晾嘶,下面提供的一些資源可以供你作進(jìn)一步學(xué)習(xí)測(cè)試使用:
既然通過(guò)本文學(xué)習(xí)你已經(jīng)學(xué)會(huì)了為你的項(xiàng)目編寫(xiě)測(cè)試妓雾,那么你下一步要了解的應(yīng)當(dāng)是自動(dòng)化測(cè)試相關(guān)的主題。為此垒迂,你可以首先學(xué)習(xí)蘋(píng)果官方的基于Xcode Server和xcodebuild的自動(dòng)測(cè)試過(guò)程(Automating the Test Process械姻,https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/08-automation.html#//apple_ref/doc/uid/TP40014132-CH7-SW1),以及發(fā)表在Wikipedia上的相關(guān)連載文章(https://en.wikipedia.org/wiki/Continuous_delivery)机断,來(lái)源于ThoughtWorks網(wǎng)站(https://www.thoughtworks.com/continuous-delivery)上的一位資深專(zhuān)家的文章楷拳。
使用Swift Playgrounds進(jìn)行測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(http://initwithstyle.net/2015/11/tdd-in-swift-playgrounds/)。你可以在Playgrounds環(huán)境下使用XCTestObservationCenter來(lái)運(yùn)行XCTestCase單元測(cè)試吏奸。你可以在Playgrounds中開(kāi)發(fā)你的工程代碼并進(jìn)行測(cè)試欢揖,然后把二者都轉(zhuǎn)換成你的應(yīng)用程序。
來(lái)自CMD+U協(xié)會(huì)(http://www.cmduconf.com/)的教程告訴你如何使用PivotalCoreKit(https://github.com/pivotal/PivotalCoreKit)來(lái)測(cè)試watchOS應(yīng)用程序奋蔚。
如果你已經(jīng)編寫(xiě)了一個(gè)應(yīng)用程序她混,而只是沒(méi)有為它編寫(xiě)測(cè)試,你可以參閱Michael Feathers的圖書(shū)《Working Effectively with Legacy Code》(https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052/ref=sr_1_1?s=books&ie=UTF8&qid=1481511568&sr=1-1)泊碑,因?yàn)椴话瑴y(cè)試的代碼往往都是遺留下來(lái)的代碼!
Jon Reid的高質(zhì)量編碼示例編程文章(http://qualitycoding.org/tdd-sample-archives/)也是你學(xué)習(xí)測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的極好去處坤按。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市馒过,隨后出現(xiàn)的幾起案子臭脓,更是在濱河造成了極大的恐慌,老刑警劉巖腹忽,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件来累,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡窘奏,警方通過(guò)查閱死者的電腦和手機(jī)佃扼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蔼夜,“玉大人兼耀,你說(shuō)我怎么就攤上這事。” “怎么了瘤运?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵窍霞,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拯坟,道長(zhǎng)但金,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任郁季,我火速辦了婚禮冷溃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘梦裂。我一直安慰自己似枕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布年柠。 她就那樣靜靜地躺著凿歼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪冗恨。 梳的紋絲不亂的頭發(fā)上答憔,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音掀抹,去河邊找鬼虐拓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛傲武,可吹牛的內(nèi)容都是我干的侯嘀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼谱轨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼戒幔!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起土童,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤诗茎,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后献汗,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體敢订,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年罢吃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了楚午。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡尿招,死狀恐怖矾柜,靈堂內(nèi)的尸體忽然破棺而出阱驾,到底是詐尸還是另有隱情,我是刑警寧澤怪蔑,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布里覆,位于F島的核電站,受9級(jí)特大地震影響缆瓣,放射性物質(zhì)發(fā)生泄漏喧枷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一弓坞、第九天 我趴在偏房一處隱蔽的房頂上張望隧甚。 院中可真熱鬧,春花似錦渡冻、人聲如沸戚扳。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至茬腿,卻和暖如春呼奢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背切平。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工握础, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人悴品。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓禀综,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親苔严。 傳聞我的和親對(duì)象是個(gè)殘疾皇子定枷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)届氢,斷路器欠窒,智...
    卡卡羅2017閱讀 134,659評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,163評(píng)論 25 707
  • 怎么樣花錢(qián)最開(kāi)心 1.目的 從這篇文章得到我賺錢(qián)的目的就是運(yùn)用錢(qián)去換取一些能給我?guī)?lái)快樂(lè)的東西,根據(jù)三腦原理-我們...
    思遠(yuǎn)同學(xué)閱讀 153評(píng)論 2 0
  • 文/懶橘子 1.相識(shí) 對(duì)“小蠻腰”的印象始于6年前退子,一個(gè)初夏的下午岖妄,《中國(guó)國(guó)...
    懶橘子閱讀 663評(píng)論 2 4
  • 【人物:A(女)B(男)】 【 關(guān)系:不詳】 【地點(diǎn):遙遠(yuǎn)】 【時(shí)間:深夜】 A:好想喝酒,一醉方休寂祥。 B:又怎么...
    何鵬在簡(jiǎn)書(shū)閱讀 723評(píng)論 0 3