自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

譯文
移動(dòng)開(kāi)發(fā) iOS
當(dāng)今世界,測(cè)試成為軟件工程開(kāi)發(fā)的必需環(huán)節(jié)。高質(zhì)量的單元測(cè)試與UI測(cè)試可以確保您的軟件無(wú)后顧之憂,特別是把一些高質(zhì)量函數(shù)或者模塊納入您的代碼倉(cāng)庫(kù)中重用之時(shí)。在本篇中,我們將向您全面細(xì)致地介紹基于iOS平臺(tái)進(jìn)行單元測(cè)試和UI測(cè)試的完整過(guò)程及相關(guān)技巧。

【51CTO.com快譯】編寫(xiě)測(cè)試可不是一項(xiàng)迷人的工作;然而,由于測(cè)試可以避免使你的寶貝應(yīng)用程序變成一塊充斥錯(cuò)誤的大垃圾場(chǎng),所以編寫(xiě)測(cè)試又是一項(xiàng)非常有必要做的工作。如果你正在閱讀本文,那么你應(yīng)當(dāng)已經(jīng)知道你應(yīng)該為您的代碼和用戶界面編寫(xiě)測(cè)試,只是不確定如何在Xcode中編寫(xiě)測(cè)試。

[[186816]]

也許你已經(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è)試用戶界面和性能,以及如何使用代碼覆蓋工具。隨著文章的展開(kāi),你會(huì)不斷熟悉一些與測(cè)試相關(guān)的術(shù)語(yǔ),到文章結(jié)尾時(shí)你會(huì)沉著地把依賴關(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)容︰

  • 核心功能︰模型類和方法及其與控制器的交互
  • 最常見(jiàn)的用戶界面工作流
  • 邊界條件
  • 錯(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類中,并相應(yīng)地添加了另一種游戲風(fēng)格。

在游戲的右下角提供了一個(gè)分段的控制器組件,供用戶選擇游戲風(fēng)格︰或者是Slide類型,允許玩家移動(dòng)滑塊組件以盡可能接近目標(biāo)值;或者是Type類型,允許玩家猜測(cè)滑塊到達(dá)的位置??丶鄳?yīng)的動(dòng)作代碼中還會(huì)將用戶選擇的游戲風(fēng)格存儲(chǔ)為該用戶的默認(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版本。用戶可以使用iTunes API查詢歌曲,然后下載并播放對(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)的名稱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è)子類BullsEyeTests,同時(shí)提供了setup()方法,tearDown()方法,還有系統(tǒng)默認(rèn)的示例測(cè)試方法。

歸納起來(lái),共有三種辦法可以運(yùn)行測(cè)試類:

1. 使用命令Product\Test或者Command-U;這將會(huì)運(yùn)行所有的測(cè)試類。

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ǔ)句下方添加下面這一行代碼︰

  1. @testable import BullsEye 

這一行代碼使單元測(cè)試能夠訪問(wèn)到BullsEye中的類和方法。

接下來(lái),請(qǐng)?jiān)贐ullsEyeTests類的頂部添加下面的屬性:

  1. var gameUnderTest: BullsEyeGame! 

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

  1. gameUnderTest = BullsEyeGame() 
  2.  
  3. gameUnderTest.startNewGame() 

 

上面的代碼將創(chuàng)建一個(gè)類級(jí)的SUT(System Under Test,測(cè)試系統(tǒng))對(duì)象。這樣一來(lái),測(cè)試類中的所有測(cè)試都可以訪問(wèn)該SUT對(duì)象的屬性和方法。

在這里,你還可以調(diào)用游戲的startNewGame方法——此方法只創(chuàng)建一個(gè)targetValue值。您的很多測(cè)試都將使用這個(gè)targetValue值,來(lái)測(cè)試程序能夠正確計(jì)算出游戲中的得分。

最后,切記在tearDown()方法中在調(diào)用超類前釋放掉你的SUT對(duì)象︰

  1. 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():

  1. // XCTAssert to test model 
  2. func testScoreIsComputed() { 
  3.   // 1. given 
  4.   let guess = gameUnderTest.targetValue + 5 
  5.   
  6.   // 2. when 
  7.   _ = gameUnderTest.check(guess: guess) 
  8.   
  9.   // 3. then 
  10.   XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong"

 

測(cè)試方法的名稱總是以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)站提供的按類別提供的斷言列表

(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)。

[[186817]]

另外,上述測(cè)試中的Given-When-Then結(jié)構(gòu)來(lái)源于行為驅(qū)動(dòng)測(cè)試(Behavior Driven Development,簡(jiǎn)稱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)下列代碼:

  1. func testScoreIsComputedWhenGuessLTTarget() { 
  2.   // 1. given 
  3.   let guess = gameUnderTest.targetValue - 5 
  4.   
  5.   // 2. when 
  6.   _ = gameUnderTest.check(guess: guess) 
  7.   
  8.   // 3. then 
  9.   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(_:)中取消正確代碼前面的注釋,并刪除不正確的代碼即可。

刪除上面設(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)查詢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程序:

  1. @testable import HalfTunes 

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

  1. var sessionUnderTest: URLSession! 
  2. override func setUp() { 
  3.   super.setUp() 
  4.   sessionUnderTest = URLSession(configuration: URLSessionConfiguration.default
  5. override func tearDown() { 
  6.   sessionUnderTest = nil 
  7.   super.tearDown() 

 

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

  1. //異步測(cè)試時(shí):成功測(cè)試很快,失敗測(cè)試卻比較慢 
  2. func testValidCallToiTunesGetsHTTPStatusCode200() { 
  3.   // given 
  4.   let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba"
  5.   // 1 
  6.   let promise = expectation(description: "Status code: 200"
  7.   
  8.   // when 
  9.   let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in 
  10.     // then 
  11.     if let error = error { 
  12.       XCTFail("Error: \(error.localizedDescription)"
  13.       return 
  14.     } else if let statusCode = (response as? HTTPURLResponse)?.statusCode { 
  15.       if statusCode == 200 { 
  16.         // 2 
  17.         promise.fulfill() 
  18.       } else { 
  19.         XCTFail("Status code: \(statusCode)"
  20.       } 
  21.     } 
  22.   } 
  23.   dataTask.resume() 
  24.   // 3 
  25.   waitForExpectations(timeout: 5, handler: nil) 

 

上面這個(gè)測(cè)試的目的是檢查發(fā)送到iTunes的有效的查詢是否能夠返回狀態(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í)間?,F(xiàn)在,我們來(lái)解決如何快速確定是否您的測(cè)試失敗的問(wèn)題。

為了修改一下您的測(cè)試,從而導(dǎo)致異步操作時(shí)失敗,你只需要從下面的URL中刪除“itunes”一詞后面的s字母即可:

  1. 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è)試添加到您的類中︰

  1. // Asynchronous test: faster fail 
  2. func testCallToiTunesCompletes() { 
  3.   // given 
  4.   let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba"
  5.   // 1 
  6.   let promise = expectation(description: "Completion handler invoked"
  7.   var statusCode: Int
  8.   var responseError: Error? 
  9.   
  10.   // when 
  11.   let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in 
  12.     statusCode = (response as? HTTPURLResponse)?.statusCode 
  13.     responseError = error 
  14.     // 2 
  15.     promise.fulfill() 
  16.   } 
  17.   dataTask.resume() 
  18.   // 3 
  19.   waitForExpectations(timeout: 5, handler: nil) 
  20.   
  21.   // then 
  22.   XCTAssertNil(responseError) 
  23.   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)您的代碼依賴于一個(gè)系統(tǒng)或庫(kù)中的對(duì)象時(shí),通過(guò)上面?zhèn)卧斓霓k法可以創(chuàng)建一個(gè)假的對(duì)象來(lái)實(shí)現(xiàn)那一部分功能并把這種偽造注入到您的代碼中。喬恩·里德的依賴性注入技術(shù)文章(https://www.objc.io/issues/15-testing/dependency-injection/)中就介紹了好幾種方法來(lái)達(dá)到這一目的。

[[186818]]

從代理(stub)中偽造輸入

在本節(jié)中的測(cè)試中,你將要檢查應(yīng)用程序的updateSearchResults(_:)方法能夠正確解析由會(huì)話下載的數(shù)據(jù)——通過(guò)檢查屬性searchResults.count的值是正確的來(lái)實(shí)現(xiàn)。SUT是視圖控制器;你要使用代理(stub)技術(shù)來(lái)偽裝一個(gè)會(huì)話和一些預(yù)先下載的數(shù)據(jù)。

為此,從“+”菜單下選擇命令“New Unit Test Target…”并命名它為HalfTunesFakeTests。然后,在import語(yǔ)句的下面導(dǎo)入HalfTunes程序:

  1. @testable import HalfTunes 

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

  1. var controllerUnderTest: SearchViewController! 
  2.   
  3. override func setUp() { 
  4.   super.setUp() 
  5.   controllerUnderTest = UIStoryboard(name"Main",  
  6.       bundle: nil).instantiateInitialViewController() as! SearchViewController! 
  7.   
  8. override func tearDown() { 
  9.   controllerUnderTest = nil 
  10.   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ì)話提供給你的測(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或類似的文件。你可以預(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ì)話對(duì)象;這些都實(shí)現(xiàn)于方法setup()中,相應(yīng)的代碼位于創(chuàng)建SUT對(duì)象的語(yǔ)句之后:

  1. let testBundle = Bundle(for: type(of: self)) 
  2. let path = testBundle.path(forResource: "abbaData", ofType: "json"
  3. let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped) 
  4.   
  5. let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba"
  6. let urlResponse = HTTPURLResponse(url: url!, statusCode: 200, httpVersion: nil, headerFields: nil) 
  7.   
  8. let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil) 
  9. At the end of setup(), inject the fake session into the app as a property of the SUT: 
  10.  
  11. controllerUnderTest.defaultSession = sessionMock 

 

【注意】您將直接在您的測(cè)試中使用偽造的會(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)容︰

  1. //使用DHURLSession協(xié)議和代理偽造URLSession 
  2. func test_UpdateSearchResults_ParsesData() { 
  3.   // given 
  4.   let promise = expectation(description: "Status code: 200"
  5.   
  6.   // when 
  7.   XCTAssertEqual(controllerUnderTest?.searchResults.count, 0, "searchResults should be empty before the data task runs"
  8.   let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba"
  9.   let dataTask = controllerUnderTest?.defaultSession.dataTask(with: url!) { 
  10.     data, response, error in 
  11.     // if HTTP request is successful, call updateSearchResults(_:) which parses the response data into Tracks 
  12.     if let error = error { 
  13.       print(error.localizedDescription) 
  14.     } else if let httpResponse = response as? HTTPURLResponse { 
  15.       if httpResponse.statusCode == 200 { 
  16.         promise.fulfill() 
  17.         self.controllerUnderTest?.updateSearchResults(data) 
  18.       } 
  19.     } 
  20.   } 
  21.   dataTask?.resume() 
  22.   waitForExpectations(timeout: 5, handler: nil) 
  23.   
  24.   // then 
  25.   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)格:用戶可以選擇移動(dòng)滑塊來(lái)匹配目標(biāo)值或從滑塊位置猜測(cè)目標(biāo)值。借助于界面右下角的分段控制開(kāi)關(guān)可以切換游戲風(fēng)格并更新用戶默認(rèn)的游戲風(fēng)格。

你要編寫(xiě)的下一個(gè)測(cè)試將檢查應(yīng)用程序能夠正確地更新用戶默認(rèn)的游戲風(fēng)格數(shù)據(jù)。

在測(cè)試導(dǎo)航器中,點(diǎn)擊命令“New Unit Test Target…”,并命名為BullsEyeMockTests。然后,在導(dǎo)入語(yǔ)句下面添加以下內(nèi)容:

  1. @testable import BullsEye 
  2.   
  3. class MockUserDefaults: UserDefaults { 
  4.   var gameStyleChanged = 0 
  5.   override func set(_ value: Int, forKey defaultName: String) { 
  6.     if defaultName == "gameStyle" { 
  7.       gameStyleChanged += 1 
  8.     } 
  9.   } 

 

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

在BullsEyeMockTests類中聲明SUT對(duì)象和模擬對(duì)象:

  1. var controllerUnderTest: ViewController! 
  2. var mockUserDefaults: MockUserDefaults! 

 

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

  1. controllerUnderTest = UIStoryboard(name"Main", bundle: nil).instantiateInitialViewController() as! ViewController! 
  2. mockUserDefaults = MockUserDefaults(suiteName: "testing")! 
  3. controllerUnderTest.defaults = mockUserDefaults 
  4. Release the SUT and the mock object in tearDown(): 
  5. controllerUnderTest = nil 
  6. mockUserDefaults = nil 
  7. Replace testExample() with this: 
  8. // Mock to test interaction with UserDefaults 
  9. func testGameStyleCanBeChanged() { 
  10.   // given 
  11.   let segmentedControl = UISegmentedControl() 
  12.   
  13.   // when 
  14.   XCTAssertEqual(mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions"
  15.   segmentedControl.addTarget(controllerUnderTest,  
  16.       action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged) 
  17.   segmentedControl.sendActions(for: .valueChanged) 
  18.   
  19.   // then 
  20.   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ò)查詢來(lái)查找一個(gè)應(yīng)用程序的UI對(duì)象,進(jìn)而合成事件,然后將這些事件發(fā)送給這些對(duì)象。其提供的API使您可以檢查一個(gè)用戶界面對(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)名稱BullsEyeUITests。

然后,在BullsEyeUITests類的頂部添加如下屬性︰

  1. var app: XCUIApplication! 

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

  1. app = XCUIApplication() 
  2.  
  3. app.launch() 

 

把testExample()的名字更改為testGameStyleSwitch()。

然后,在testGameStyleSwitch()中按下回車鍵創(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()中擁有以下三行代碼︰

  1. let app = XCUIApplication() 
  2.  
  3. app.buttons["Slide"].tap() 
  4.  
  5. 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"]。

于是,你有了如下的代碼:

  1. app.segmentedControls.buttons["Slide"
  2.  
  3. app.staticTexts["Get as close as you can to: "

 

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

  1. // given 
  2.  
  3. let slideButton = app.segmentedControls.buttons["Slide"
  4.  
  5. let typeButton = app.segmentedControls.buttons["Type"
  6.  
  7. let slideLabel = app.staticTexts["Get as close as you can to: "
  8.  
  9. let typeLabel = app.staticTexts["Guess where the slider is: "

 

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

  1. // then 
  2.  
  3. if slideButton.isSelected { 
  4.  
  5. XCTAssertTrue(slideLabel.exists) 
  6.  
  7. XCTAssertFalse(typeLabel.exists) 
  8.  
  9. typeButton.tap() 
  10.  
  11. XCTAssertTrue(typeLabel.exists) 
  12.  
  13. XCTAssertFalse(slideLabel.exists) 
  14.  
  15. else if typeButton.isSelected { 
  16.  
  17. XCTAssertTrue(typeLabel.exists) 
  18.  
  19. XCTAssertFalse(slideLabel.exists) 
  20.  
  21. slideButton.tap() 
  22.  
  23. XCTAssertTrue(slideLabel.exists) 
  24.  
  25. XCTAssertFalse(typeLabel.exists) 
  26.  

 

這段代碼將會(huì)檢測(cè)當(dāng)選中或者點(diǎn)擊每個(gè)按鈕時(shí)是否存在正確的標(biāo)簽?,F(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類中使用下面的測(cè)試,從而替換掉系統(tǒng)默認(rèn)生成的testPerformanceExample()方法︰

  1. // Performance  
  2. func test_StartDownload_Performance() { 
  3.   let track = Track(name"Waterloo", artist: "ABBA",  
  4.       previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a"
  5.   measure { 
  6.     self.controllerUnderTest?.startDownload(track) 
  7.   } 

 

現(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)致性能的下降……以線性方式影響代碼的執(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ì)搜索到有贊同的也有反對(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)上的一位資深專家的文章。
  • 使用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ā)的極好去處。

【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文譯者和出處為51CTO.com】

 

責(zé)任編輯:龐桂玉 來(lái)源: 51CTO.com
相關(guān)推薦

2020-05-07 17:30:49

開(kāi)發(fā)iOS技術(shù)

2017-01-14 23:42:49

單元測(cè)試框架軟件測(cè)試

2017-02-23 15:59:53

測(cè)試MockSetup

2020-08-18 08:10:02

單元測(cè)試Java

2012-05-21 09:41:54

XcodeiOS單元測(cè)試

2017-01-14 23:26:17

單元測(cè)試JUnit測(cè)試

2017-01-16 12:12:29

單元測(cè)試JUnit

2011-07-27 17:02:12

Xcode iPhone 單元測(cè)試

2011-06-20 17:25:02

單元測(cè)試

2011-05-16 16:52:09

單元測(cè)試徹底測(cè)試

2017-03-23 16:02:10

Mock技術(shù)單元測(cè)試

2021-05-05 11:38:40

TestNGPowerMock單元測(cè)試

2023-07-26 08:58:45

Golang單元測(cè)試

2011-07-04 18:16:42

單元測(cè)試

2010-10-09 14:43:46

J2MEJUnit

2021-09-03 08:57:59

Swift強(qiáng)制解析

2023-08-02 13:59:00

GoogleTestCTest單元測(cè)試

2015-05-08 10:29:59

OCMockiOS測(cè)試

2022-04-27 08:17:07

OCMock單元測(cè)試集成

2011-04-18 13:20:40

單元測(cè)試軟件測(cè)試
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)