自動化Test使用詳細解析(二) —— 單元測試和UI Test使用簡單示例(一)

版本記錄

版本號 時間
V1.0 2019.04.19 星期五

前言

自動化Test可以通過編寫代碼滞诺、或者是記錄開發(fā)者的操作過程并代碼化定拟,來實現(xiàn)自動化測試等功能尼啡。接下來幾篇我們就說一下該技術(shù)的使用琐凭。感興趣的可以看下面幾篇芽隆。
1. 自動化Test使用詳細解析(一) —— 基本使用(一)

開始

首先看下寫作環(huán)境

Swift 4.2, iOS 12, Xcode 10

本篇主要了解如何將單元測試和UI測試添加到iOS應用程序,以及如何檢查代碼覆蓋率统屈。

寫作測試不是很迷人胚吁,但是由于測試可以防止讓你的閃亮應用程序變成了一個充滿bug的垃圾,這是必要的愁憔。 如果您正在閱讀本教程腕扶,您已經(jīng)知道您應該為您的代碼和UI編寫測試,但您可能不知道如何去做吨掌。

您可能有一個有效的應用程序半抱,但您想測試您為擴展應用程序所做的更改脓恕。 也許您已經(jīng)編寫了測試,但不確定它們是否是正確的測試代虾。 或者进肯,您已經(jīng)開始研究新的應用程序,并希望隨時進行測試棉磨。

本教程將向您展示:

  • 如何使用Xcode的Test導航器測試應用程序的模型和異步方法
  • 如何使用stubs and mocks與庫或系統(tǒng)對象的交互
  • 如何測試UI和性能
  • 如何使用代碼覆蓋工具

在此過程中江掩,您將獲得測試ninjas所使用的一些詞匯。


Figuring Out What to Test

在編寫任何測試之前乘瓤,了解基礎(chǔ)知識非常重要环形。 你需要測試什么?

如果您的目標是擴展現(xiàn)有應用程序衙傀,則應首先為計劃更改的任何組件編寫測試抬吟。

通常,測試應包括:

  • 核心功能:模型類和方法及其與控制器的交互
  • 最常見的UI工作流程
  • 邊界條件
  • Bug修復

1. Best Practices for Testing

首字母縮略詞FIRST描述了有效單元測試的一套簡明標準统抬。 這些標準是:

  • Fast - 快速:測試應該快速進行火本。
  • Independent/Isolated - 獨立/隔離:測試不應彼此共享狀態(tài)。
  • Repeatable - 可重復:每次運行測試時都應獲得相同的結(jié)果聪建。 外部數(shù)據(jù)提供者或并發(fā)問題可能導致間歇性故障钙畔。
  • Self-validating - 自我驗證:測試應完全自動化。 輸出應該是“通過”或“失敗”金麸,而不是依賴于程序員對日志文件的解釋擎析。
  • Timely - 及時:理想情況下,應在編寫測試的生產(chǎn)代碼(測試驅(qū)動開發(fā))之前編寫測試挥下。

遵循FIRST原則將使您的測試保持清晰且有用揍魂,而不是為您的應用程序設(shè)置障礙。

打開已經(jīng)下載的工程文件:

  • BullsEye基于iOS Apprentice中的示例應用程序棚瘟。 游戲邏輯位于BullsEyeGame類中现斋,您將在本教程中測試它。
  • HalfTunesURLSession教程中示例應用程序的更新版本偎蘸。 用戶可以在iTunes API中查詢歌曲庄蹋,然后下載和播放歌曲片段。

Unit Testing in Xcode

Test navigator提供了最簡單的測試方法禀苦,您將使用它來創(chuàng)建測試目標并針對您的應用程序運行測試蔓肯。

1. Creating a Unit Test Target

打開BullsEye項目并按Command-6打開Test navigator遂鹊。

單擊左下角的+按鈕振乏,然后從菜單中選擇New Unit Test Target ...

接受默認名稱BullsEyeTests。 當test bundle出現(xiàn)在Test navigator中時秉扑,單擊以在編輯器中打開該包慧邮。 如果bundle未自動顯示调限,請單擊其他導航器之一進行故障排除,然后返回Test navigator误澳。

默認模板導入測試框架XCTest耻矮,并使用setUp()tearDown()和示例測試方法定義XCTestCaseBullsEyeTests子類忆谓。

運行測試有三種方法:

  • 1) Product ? Test or Command-U裆装。 這兩個都運行所有測試類。
  • 2) 單擊Test navigator中的箭頭按鈕倡缠。
  • 3) 單擊gutter中的菱形按鈕哨免。

您還可以通過在Test navigatorgutter中單擊其菱形來運行單個測試方法。

嘗試不同的方式來運行測試昙沦,以了解它需要多長時間以及它看起來像什么琢唾。 樣本測試還沒有做任何事情,所以它們運行得非扯芤快采桃!

當所有測試成功后,菱形將變?yōu)榫G色并顯示復選標記丘损。 您可以單擊testPerformanceExample()末尾的灰色菱形以打開性能結(jié)果:

本教程不需要testPerformanceExample()testExample()普办,因此請刪除它們。

2. Using XCTAssert to Test Models

首先号俐,您將使用XCTAssert函數(shù)來測試BullsEye模型的核心功能:BullsEyeGame對象是否正確計算了一輪的分數(shù)泌豆?

BullsEyeTests.swift中,在import語句下方添加以下行:

@testable import BullsEye

這使得單元測試可以訪問BullsEye中的internal類型和功能吏饿。

BullsEyeTests類的頂部踪危,添加以下屬性:

var sut: BullsEyeGame!

這為BullsEyeGame創(chuàng)建了一個占位符,它是System Under Test (SUT)猪落,或者是測試用例類與測試有關(guān)的對象贞远。

接下來,用這個替換setup()的內(nèi)容:

super.setUp()
sut = BullsEyeGame()
sut.startNewGame()

這會在類級別創(chuàng)建BullsEyeGame對象笨忌,因此此測試類中的所有測試都可以訪問SUT對象的屬性和方法蓝仲。

在這里,您還可以調(diào)用游戲的startNewGame()來初始化targetValue官疲。 許多測試將使用targetValue來測試游戲是否正確計算得分袱结。

在您忘記之前,請在tearDown()中釋放您的SUT對象途凫。 將其內(nèi)容替換為:

sut = nil
super.tearDown()

注意:最好在setUp()中創(chuàng)建SUT并在tearDown()中釋放它垢夹,以確保每個測試都以一個干凈的平板開始。 有關(guān)更多討論维费,請查看Jon Reid’s post關(guān)于此主題的帖子果元。


Writing Your First Test

現(xiàn)在促王,您已經(jīng)準備好編寫第一個測試了!

將以下代碼添加到BullsEyeTests的末尾:

func testScoreIsComputed() {
  // 1. given
  let guess = sut.targetValue + 5

  // 2. when
  sut.check(guess: guess)

  // 3. then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

測試方法的名稱始終以test開頭而晒,然后是對其測試內(nèi)容的描述蝇狼。

將測試格式化為given, when and then的部分是一種很好的做法:

  • Given:在這里,您可以設(shè)置所需的任何值倡怎。 在此示例中迅耘,您將創(chuàng)建一個guess值,以便指定它與targetValue的差異监署。
  • When:在本節(jié)中豹障,您將執(zhí)行正在測試的代碼:調(diào)用check(guess :)
  • Then:這是您將斷言您期望的結(jié)果的部分焦匈,如果測試失敗則打印一條消息血公。 在這種情況下,sut.scoreRound應該等于95(100 - 5)缓熟。

單擊gutterTest navigator中的菱形圖標運行測試累魔。 這將構(gòu)建并運行應用程序,菱形圖標將變?yōu)榫G色復選標記够滑!

注意:要查看XCTestAssertions的完整列表垦写,請轉(zhuǎn)到 Apple’s Assertions Listed by Category

1. Debugging a Test

BullsEyeGame故意內(nèi)置了一個錯誤彰触,你現(xiàn)在就可以練習找到它梯投。 要查看操作中的bug,您將創(chuàng)建一個測試况毅,從given部分中的targetValue中減去5分蓖,并使其他所有內(nèi)容保持不變。

添加以下測試:

func testScoreIsComputedWhenGuessLTTarget() {
  // 1. given
  let guess = sut.targetValue - 5

  // 2. when
  sut.check(guess: guess)

  // 3. then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

guesstargetValue之間的差異仍為5尔许,因此得分仍應為95么鹤。

在“斷點”導航器Breakpoint navigator中,添加Test Failure Breakpoint味廊。 當測試方法發(fā)布失敗assertion時蒸甜,這將停止測試運行。

運行測試余佛,它應該在測試失敗時停在XCTAssertEqual行柠新。

在調(diào)試控制臺中檢查sutguess

guesstargetValue - 5但是scoreRound105,而不是95辉巡!

要進一步調(diào)查恨憎,請使用正常的調(diào)試過程:在when語句中設(shè)置一個斷點,在BullsEyeGame.swift中設(shè)置一個斷點红氯,在check(guess :)中框咙,它會產(chǎn)生difference。 然后再次運行測試痢甘,并跳過let difference語句以檢查應用程序中difference的值:

問題是difference是負值喇嘱,所以得分是100 - ( - 5)。 要解決此問題塞栅,您應該使用difference的絕對值者铜。 在check(guess :)中,取消注釋正確的行并刪除不正確的行放椰。

刪除兩個斷點作烟,然后再次運行測試以確認它現(xiàn)在成功。

2. Using XCTestExpectation to Test Asynchronous Operations

現(xiàn)在您已經(jīng)學會了如何測試模型和調(diào)試測試失敗砾医,現(xiàn)在是時候繼續(xù)測試異步代碼了拿撩。

打開HalfTunes項目。 它使用URLSession來查詢iTunes API并下載歌曲樣本如蚜。 假設(shè)您要修改它以使用AlamoFire進行網(wǎng)絡(luò)操作压恒。 要查看是否有任何中斷,您應該為網(wǎng)絡(luò)操作編寫測試并在更改代碼之前和之后運行它們错邦。

URLSession方法是異步的:它們立即返回探赫,但直到稍后才完成運行。 要測試異步方法撬呢,請使用XCTestExpectation使測試等待異步操作完成伦吠。

異步測試通常很慢,因此您應該將它們與更快的單元測試分開魂拦。

創(chuàng)建一個名為HalfTunesSlowTests的新單元測試目標毛仪。 打開HalfTunesSlowTests類,并在現(xiàn)有import語句下方導入HalfTunes應用程序模塊:

@testable import HalfTunes

此類中的所有測試都使用默認的URLSession將請求發(fā)送到Apple的服務器芯勘,因此聲明一個sut對象潭千,在setUp()中創(chuàng)建它并在tearDown()中釋放它。

用以下內(nèi)容替換HalfTunesSlowTests類的內(nèi)容:

var sut: URLSession!

override func setUp() {
  super.setUp()
  sut = URLSession(configuration: .default)
}

override func tearDown() {
  sut = nil
  super.tearDown()
}

下面借尿,添加異步測試:

// 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 = sut.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
  wait(for: [promise], timeout: 5)
}

此測試檢查向iTunes發(fā)送有效查詢是否返回200狀態(tài)代碼刨晴。 大多數(shù)代碼與您在應用程序中編寫的代碼相同,使用以下附加行:

  • 1) expectation(description :):返回存儲在promise中的XCTestExpectation對象路翻。 description參數(shù)描述了您期望發(fā)生的事情狈癞。
  • 2) promise.fulfill():在異步方法的完成處理程序的成功條件閉包中調(diào)用它來標記已滿足期望。
  • 3) wait(for:timeout :):保持測試運行茂契,直到滿足所有期望蝶桶,或超時間隔timeout結(jié)束,以先發(fā)生者為準掉冶。

運行測試真竖。 如果您已連接到互聯(lián)網(wǎng)脐雪,則應用程序在模擬器中加載后,測試應該需要大約一秒鐘才能成功恢共。

3. Failing Fast

失敗會傷害战秋,但它不需要永遠。

要體驗失敗讨韭,只需從URL中的“itunes”中刪除's'即可:

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

運行測試脂信。 它失敗了,但需要完整的超時間隔透硝! 這是因為你假設(shè)請求總是成功狰闪,那就是你調(diào)用promise.fulfill()的地方。 由于請求失敗濒生,僅在超時到期時才完成埋泵。

您可以通過更改以下假設(shè)來改進此問題并使測試失敗:不要等待請求成功罪治,而是等待直到調(diào)用異步方法的完成處理程序秋泄。 一旦應用程序收到來自服務器的響應(OK或錯誤),就會發(fā)生這種情況规阀,這符合預期恒序。 然后,您的測試可以檢查請求是否成功谁撼。

要了解其工作原理歧胁,請創(chuàng)建一個新測試。

但首先厉碟,通過撤消對url所做的更改來修復上一個測試喊巍。

然后,將以下測試添加到您的類:

func testCallToiTunesCompletes() {
  // given
  let url = 
    URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
  let promise = expectation(description: "Completion handler invoked")
  var statusCode: Int?
  var responseError: Error?

  // when
  let dataTask = sut.dataTask(with: url!) { data, response, error in
    statusCode = (response as? HTTPURLResponse)?.statusCode
    responseError = error
    promise.fulfill()
  }
  dataTask.resume()
  wait(for: [promise], timeout: 5)

  // then
  XCTAssertNil(responseError)
  XCTAssertEqual(statusCode, 200)
}

關(guān)鍵的區(qū)別在于箍鼓,簡單地輸入完成處理程序就能滿足期望崭参,而這只需要大約一秒鐘的時間。 如果請求失敗款咖,則then斷言失敗何暮。

運行測試。 它現(xiàn)在應該花費大約一秒鐘才能失敗铐殃。 它失敗是因為請求失敗海洼,而不是因為測試運行超過了timeout

修復url富腊,然后再次運行測試以確認它現(xiàn)在成功坏逢。

后記

本篇主要介紹了單元測試和UI Test使用簡單示例,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市是整,隨后出現(xiàn)的幾起案子肖揣,更是在濱河造成了極大的恐慌,老刑警劉巖浮入,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件龙优,死亡現(xiàn)場離奇詭異,居然都是意外死亡舵盈,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進店門球化,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秽晚,“玉大人,你說我怎么就攤上這事筒愚「坝” “怎么了?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵巢掺,是天一觀的道長句伶。 經(jīng)常有香客問我,道長陆淀,這世上最難降的妖魔是什么考余? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮轧苫,結(jié)果婚禮上楚堤,老公的妹妹穿的比我還像新娘。我一直安慰自己含懊,他們只是感情好身冬,可當我...
    茶點故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著岔乔,像睡著了一般酥筝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上雏门,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天嘿歌,我揣著相機與錄音,去河邊找鬼茁影。 笑死搅幅,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的呼胚。 我是一名探鬼主播茄唐,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沪编?” 一聲冷哼從身側(cè)響起呼盆,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蚁廓,沒想到半個月后访圃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡相嵌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年腿时,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饭宾。...
    茶點故事閱讀 38,683評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡批糟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出看铆,到底是詐尸還是另有隱情徽鼎,我是刑警寧澤,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布弹惦,位于F島的核電站否淤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏棠隐。R本人自食惡果不足惜石抡,卻給世界環(huán)境...
    茶點故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望助泽。 院中可真熱鬧汁雷,春花似錦、人聲如沸报咳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽暑刃。三九已至厢漩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間岩臣,已是汗流浹背溜嗜。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留架谎,地道東北人炸宵。 一個月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像谷扣,于是被迫代替她去往敵國和親土全。 傳聞我的和親對象是個殘疾皇子捎琐,可洞房花燭夜當晚...
    茶點故事閱讀 43,566評論 2 349

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