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

版本記錄

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

前言

自動化Test可以通過編寫代碼、或者是記錄開發(fā)者的操作過程并代碼化,來實現(xiàn)自動化測試等功能。接下來幾篇我們就說一下該技術(shù)的使用。感興趣的可以看下面幾篇。
1. 自動化Test使用詳細解析(一) —— 基本使用(一)
2. 自動化Test使用詳細解析(二) —— 單元測試和UI Test使用簡單示例(一)

Faking Objects and Interactions

異步測試使您確信您的代碼會為異步API生成正確的輸入。您可能還希望在從URLSession接收輸入時測試您的代碼是否正常工作,或者它是否正確更新了用戶的默認數(shù)據(jù)庫或iCloud容器。

大多數(shù)應用程序與系統(tǒng)或庫對象(您無法控制的對象)進行交互,與這些對象交互的測試可能很慢且不可重復,違反了兩個FIRST原則。相反,您可以通過從stubs獲取輸入或通過更新模擬mock對象來偽造交互。

當代碼依賴于系統(tǒng)或庫對象時,請使用偽造。您可以通過創(chuàng)建一個假對象來播放該部分并將此偽注入您的代碼中來實現(xiàn)此目的。 Jon ReidDependency Injection描述了幾種方法。

1. Fake Input From Stub

在此測試中,您將通過檢查searchResults.count是否正確來檢查應用程序的updateSearchResults(_ :)是否正確解析會話下載的數(shù)據(jù)。 SUT是視圖控制器,您將使用stubs和一些預先下載的數(shù)據(jù)偽造會話。

轉(zhuǎn)到Test navigator并添加新的Unit Test Target。將其命名為HalfTunesFakeTests。打開HalfTunesFakeTests.swift并導入import語句正下方的HalfTunes應用程序模塊:

@testable import HalfTunes

現(xiàn)在,用以下內(nèi)容替換HalfTunesFakeTests類的內(nèi)容:

var sut: SearchViewController!

override func setUp() {
  super.setUp()
  sut = UIStoryboard(name: "Main", bundle: nil)
    .instantiateInitialViewController() as? SearchViewController
}

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

這聲明了SUT,它是一個SearchViewController,在setUp()中創(chuàng)建它并在tearDown()中釋放它:

注意:SUT是視圖控制器,因為HalfTunes有一個巨大的視圖控制器問題 - 所有工作都在SearchViewController.swift中完成。 Moving the networking code into a separate module可以減少此問題,并且還可以使測試更容易。

接下來,您將需要一些示例JSON數(shù)據(jù),您的假會話將為您的測試提供這些數(shù)據(jù)。 只需要幾個項目,所以要限制你的下載結(jié)果在iTunes中附加&limit = 3URL字符串:

https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3

復制此URL并將其粘貼到瀏覽器中。 這將下載名為1.txt1.txt.js或類似文件的文件。 預覽它以確認它是一個JSON文件,然后將其重命名為abbaData.json

現(xiàn)在,返回Xcode并轉(zhuǎn)到Project navigator。 將文件添加到HalfTunesFakeTests組。

HalfTunes項目包含支持文件DHURLSessionMock.swift。 這定義了一個名為DHURLSession的簡單協(xié)議,其方法(stubs)用于創(chuàng)建具有URLURLRequest的數(shù)據(jù)任務。 它還定義了URLSessionMock,它符合此協(xié)議的初始化程序,允許您使用您選擇的數(shù)據(jù),響應和錯誤創(chuàng)建模擬URLSession對象。

要設置fake,請轉(zhuǎn)到HalfTunesFakeTests.swift并在創(chuàng)建SUT的語句之后在setUp()中添加以下內(nèi)容:

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)
sut.defaultSession = sessionMock

這將設置虛假數(shù)據(jù)和響應并創(chuàng)建虛假會話對象。 最后,它將fake會話注入應用程序作為sut的屬性。

現(xiàn)在,您已準備好編寫測試,檢查調(diào)用updateSearchResults(_ :)是否解析偽數(shù)據(jù)。 添加以下測試:

func test_UpdateSearchResults_ParsesData() {
  // given
  let promise = expectation(description: "Status code: 200")

  // when
  XCTAssertEqual(
    sut.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 = sut.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,
      httpResponse.statusCode == 200 {
      self.sut.updateSearchResults(data)
    }
    promise.fulfill()
  }
  dataTask.resume()
  wait(for: [promise], timeout: 5)

  // then
  XCTAssertEqual(sut.searchResults.count, 3, "Didn't parse 3 items from fake response")
}

您仍然必須將其寫為異步測試,因為stub是異步方法。

when斷言是在數(shù)據(jù)任務運行之前searchResults為空。這應該是真的,因為你在setUp()中創(chuàng)建了一個全新的SUT。

fake數(shù)據(jù)包含三個Track對象的JSON,因此then斷言是視圖控制器的searchResults數(shù)組包含三個項目。

運行測試。它應該很快成功,因為沒有任何真正的網(wǎng)絡連接!

2. Fake Update to Mock Object

上一個測試使用stub來提供偽對象的輸入。接下來,您將使用mock object來測試您的代碼是否正確更新了UserDefaults。

重新開啟BullsEye項目。該應用程序有兩種游戲風格:用戶可以移動滑塊以匹配目標值,也可以從滑塊位置猜測目標值。右下角的分段控件可切換游戲風格并將其保存在user defaults中。

您的下一個測試將檢查應用程序是否正確保存了gameStyle屬性。

Test navigator中,單擊New Unit Test Class并將其命名為BullsEyeMockTests。在import語句下面添加以下內(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覆蓋set(_:forKey :)以增加gameStyleChanged標志。 通常,您會看到設置Bool變量的類似測試,但增加Int會為您提供更大的靈活性 - 例如,您的測試可以檢查該方法僅被調(diào)用一次。

BullsEyeMockTests中聲明SUTmock object

var sut: ViewController!
var mockUserDefaults: MockUserDefaults!

接下來,用這個替換默認的setUp()tearDown()

override func setUp() {
  super.setUp()

  sut = UIStoryboard(name: "Main", bundle: nil)
    .instantiateInitialViewController() as? ViewController
  mockUserDefaults = MockUserDefaults(suiteName: "testing")
  sut.defaults = mockUserDefaults
}

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

這將創(chuàng)建SUTmock object,并將mock object注入為SUT的屬性。

現(xiàn)在,用以下代碼替換模板中的兩個默認測試方法:

func testGameStyleCanBeChanged() {
  // given
  let segmentedControl = UISegmentedControl()

  // when
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    0, 
    "gameStyleChanged should be 0 before sendActions")
  segmentedControl.addTarget(sut,
    action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged)
  segmentedControl.sendActions(for: .valueChanged)

  // then
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    1, 
    "gameStyle user default wasn't changed")
}

when斷言是在測試方法更改分段控件之前gameStyleChanged標志為0。 因此,如果then斷言也是真的,則意味著set(_:forKey :)只被調(diào)用一次。

運行測試;它應該成功。


UI Testing in Xcode

UI測試允許您測試與用戶界面的交互。 UI測試的工作原理是通過查詢應用程序的UI對象,合成事件,然后將事件發(fā)送到這些對象。 通過API,您可以檢查UI對象的屬性和狀態(tài),以便將它們與預期狀態(tài)進行比較。

BullsEye項目的Test navigator中,添加一個新的UI Test Target。 檢查要測試的目標是BullsEye,然后接受默認名稱BullsEyeUITests

打開BullsEyeUITests.swift并在BullsEyeUITests類的頂部添加此屬性:

var app: XCUIApplication!

setUp()中,使用以下代碼替換語句XCUIApplication().launch()

app = XCUIApplication()
app.launch()

testExample()的名稱更改為testGameStyleSwitch()。

testGameStyleSwitch()中打開一個新行,然后單擊編輯器窗口底部的紅色Record按鈕:

這將在模擬器中以一種模式打開應用程序,該模式將您的交互記錄為測試命令。 應用加載后,點按游戲風格開關(guān)的滑動Slide部分和頂部標簽。 然后,單擊Xcode Record按鈕停止錄制。

您現(xiàn)在在testGameStyleSwitch()中有以下三行:

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

Recorder已創(chuàng)建代碼來測試您在應用程序中測試的相同操作。 向sliderlabel發(fā)送一個tap。 您將使用它們作為基礎來創(chuàng)建自己的UI測試。
如果您看到任何其他語句,只需刪除它們即可。

第一行復制您在setUp()中創(chuàng)建的屬性,因此刪除該行。 你不需要點擊任何東西,所以也要刪除第2行和第3行末尾的.tap()?,F(xiàn)在,打開["Slide"]旁邊的小菜單,選擇segmentedControls.buttons [“Slide”]。

您剩下的應該是以下內(nèi)容:

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

點擊任何其他對象,讓recorder幫助您找到您可以在測試中訪問的代碼。 現(xiàn)在,用這段代碼替換這些行來創(chuàng)建給定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)在您已經(jīng)在分段控件中有兩個按鈕的名稱,以及兩個可能的頂部標簽,請在下面添加以下代碼:

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

當您在分段控件中的每個按鈕上tap()時,將檢查是否存在正確的標簽。 運行測試 - 所有斷言都應該成功。


Performance Testing

從Apple的文檔Apple’s documentation中:

A performance test takes a block of code that you want to evaluate and runs it ten times, collecting the average execution time and the standard deviation for the runs. The averaging of these individual measurements form a value for the test run that can then be compared against a baseline to evaluate success or failure.

性能測試需要一段您想要評估的代碼并運行十次,收集平均執(zhí)行時間和運行的標準偏差。 這些單獨測量的平均值形成測試運行的值,然后可以與基線進行比較以評估成功或失敗。

編寫性能測試非常簡單:只需將要測量的代碼放入measure()的閉包中即可。

要查看此操作,請重新打開HalfTunes項目,并在HalfTunesFakeTests.swift中添加以下測試:

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.sut.startDownload(track)
  }
}

運行測試,然后單擊measure()尾隨閉包開頭旁邊的圖標以查看統(tǒng)計信息。

單擊Set Baseline以設置參考時間。 然后,再次運行性能測試并查看結(jié)果 - 它可能比baseline更好或更差。 使用Edit按鈕可以將基線重置為此新結(jié)果。

每個設備配置存儲基線,因此您可以在幾個不同的設備上執(zhí)行相同的測試,并根據(jù)特定配置的處理器速度,內(nèi)存等維護不同的基線。

每當您對可能影響正在測試的方法的性能的應用程序進行更改時,請再次運行性能測試以查看它與基準的比較情況。


Code Coverage

代碼覆蓋率工具會告訴您測試實際運行的應用程序代碼,因此您知道應用程序代碼的哪些部分尚未經(jīng)過測試。

要啟用代碼覆蓋,請編輯scheme’sTest操作,并選中Options選項卡下的Gather coverage復選框:

運行所有測試(Command-U),然后打開報告導航器(Command-9)。 選擇該列表中頂部項目下的Coverage

單擊顯示三角形以查看SearchViewController.swift中的函數(shù)和閉包列表:

向下滾動到updateSearchResults(_ :)以查看覆蓋率為87.9%

單擊此功能的箭頭按鈕以打開源文件到該功能。 當您將鼠標懸停在右側(cè)邊欄中的coverage注釋上時,代碼部分會突出顯示綠色或紅色:

覆蓋注釋顯示測試命中每個代碼部分的次數(shù);未調(diào)用的部分以紅色突出顯示。 正如您所期望的那樣,for循環(huán)運行了3次,但錯誤路徑中沒有執(zhí)行任何操作。

要增加此函數(shù)的覆蓋范圍,您可以復制abbaData.json,然后對其進行編輯,以便導致不同的錯誤。 例如,將"results"更改為"result"以進行打印測試print("Results key not found in dictionary")

1. 100% Coverage?

你有多努力爭取100%的代碼覆蓋率? 谷歌“100%單元測試覆蓋率”,你會發(fā)現(xiàn)一系列支持和反對的論據(jù),以及對“100%覆蓋率”的定義的爭論。 反對它的爭論說最后10-15%不值得努力。 支持它的爭論說最后10-15%是最重要的,因為它很難測試。 谷歌“hard to unit test bad design”,以找到有說服力的論據(jù),即不可測試的代碼是更深層次設計問題的標志untestable code is a sign of deeper design problems。

以下是一些可供進一步研究的資源:

  • 有關(guān)測試主題的幾個WWDC視頻。 來自WWDC17的兩個好的是:Engineering for TestabilityTesting Tips & Tricks。
  • 下一步是自動化:Continuous Integration and Continuous Delivery。 從Apple的Xcode ServerxcodebuildAutomating the Test Process,以及維基百科的Wikipedia’s continuous delivery article,該文章借鑒了ThoughtWorks的專業(yè)知識。
  • 如果你已經(jīng)有一個應用程序但還沒有為它編寫測試,你可能想?yún)⒖?a target="_blank" rel="nofollow">Working Effectively with Legacy Code by Michael Feathers,因為沒有測試的代碼是遺留代碼!
  • Jon ReidQuality Coding樣本應用檔案非常適合學習有關(guān)Test Driven Development的更多信息。

后記

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容