開始之前
請允許先介紹在iOS開發(fā)測試中的一些基礎框架和理論:
在iOS開發(fā)的過程中毅戈,我們常接觸到的單元測試框架有 Qucik以及他的好朋友Nimble候学,前者是iOS編程開發(fā)中行為驅(qū)動開發(fā)框架,后者是對iOS平臺XCTest結果預期處理的更簡易化、人性化的封裝杂穷。
iOS的UI自動化測試乡小,則直接使用的是XCTest框架,一方面是很容易進行腳本的錄制脑漫,另一方面可以通過WebDriverAgent等三方框架接入,結合Appium以及行為描述語言Cucumber等咙崎,實現(xiàn)多語言跨端的腳本化的自動化測試优幸,此處按住不表。
再來說說測試替身(Test Double)褪猛,為了避免爭議网杆,下面上Martin Fowler對于Test Double解釋。
- Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
- Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example).
- Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
- Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
- Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting.
實戰(zhàn)
有了以上的基礎理論后,我們來逐條看這些方式在iOS編程中是如何實現(xiàn)的碳却,先做工程架構假設:
該 iOS App Swift 語言開發(fā)队秩,使用MVVM架構,
通過CocoaPods進行依賴管理昼浦,同時集成了以下三方組件:
測試組件:Quick和Nimble馍资,
彈窗組件:Toast
網(wǎng)絡基礎組件:Alamofire
以及服務模擬組件:OHHTTPStubs/Swift
Dummy
場景訴求: 我有一個頁面,布局了一個界面元素以及一個提交按鈕关噪, 為了驗證該頁面的元素是否在頁面初始化后正常加載鸟蟹,我需要通過UI自動化測試來運行工程,并在App啟動后使兔,通過腳本錄制進入到該頁面建钥,并進行頁面元素的檢查驗證。(此處只做元素是否正常顯示的驗證)虐沥。
說明: 因為使用MVVM結構锦针,在頁面進行初始化的時候,需要進行ViewModel的初始化置蜀,很明顯奈搜,在我們通過StoryBoard托拉拽期間,ViewModel是不參與邏輯的盯荤,但因為在初始化VC的時候馋吗,就需要將ViewModel綁定到VC,所以viewModel需要一個初始值來保證代碼能夠正常運行但是不參與邏輯模塊秋秤。
代碼片段:
// 初始化ViewModel
let dummyViewModel = ViewModel()
// 將其作為參數(shù)參與到ViewController的創(chuàng)建中
let viewController = ViewController(viewModel:dummyViewModel)
navgationController.push(viewController)
測試代碼:
// UITest中對于button是否顯示的判斷
let app = XCUIApplication()
app.launch()
let tablesQuery = app.tables
tablesQuery.staticTexts["商家詳情"].tap()
let trackLabel = app.staticTexts["提交"]
XCTAssertEqual(trackLabel.exists, true)
Fake
場景訴求:在真是的開發(fā)場景中宏粤,針對于前端一般都會有配套BFF服務,那么在開發(fā)的過程中灼卢,往往因為服務端開發(fā)與前端開發(fā)的進度不同步绍哎,會出現(xiàn)前端開發(fā)同學需要通過一種輕量級的實現(xiàn)來替代后端BFF,以滿足其開發(fā)階段模擬服務數(shù)據(jù)達到實現(xiàn)業(yè)務訴求的情況鞋真。
說明:在上一例子中崇堰,我們再頁面里選擇了幾個checklist選項 ,并點擊提交按鈕涩咖,此時需要調(diào)用API服務發(fā)起訂單提交請求海诲,此時會有這樣一個場景:提交成功。假設我們與后端開發(fā)已經(jīng)進行了接口API約定檩互,定義了正常處理的返回數(shù)據(jù)結構特幔,則可以通過啟用一個輕量級實現(xiàn)的MockServer,返回特定結果闸昨,幫助我們完成Service層的邏輯開發(fā)蚯斯。
代碼片段:
// Services 層代碼:
var shoppingCart: Dictionary<Food, Int> = Dictionary()
func checkout(success: @escaping successCallback, fail: @escaping failCallback) {
service.checkoutService(shoppingCart) {
success()
} failure: { error in
fail(error)
}
}
測試代碼:
// Test 部分代碼:
let service = CheckoutService()
context("checkout") {
// 工序X fake BFF薄风,實現(xiàn)service
it("should be callback success when call BFF success") {
stub(condition: isHost("127.0.0.0")) { _ in
// loading 成功的 json文件
let stubPath = OHPathForFile("checkoutSuccess.json", type(of: self))
// 在OHHTTPStubs中,返回http 200結果拍嵌,并將成功的結果通過接口返回
return fixture(filePath: stubPath!, status: 200, headers: ["Content-Type": "application/json"])
}
waitUntil(timeout: .seconds(5)) { done in
// 在service中進行 checkout 服務調(diào)用村刨,并等待5秒等待成功的返回結果。
service.checkout(Dictionary<Food, Int>()) {
done()
} failure: { error in
}
}
}
Mock
場景訴求:在業(yè)務場景中撰茎,我們經(jīng)常需要根據(jù)某種操作的異常case,通過UI頁面對用戶進行Toast提示打洼,比如龄糊,在進行業(yè)務的提交處理時,因為數(shù)據(jù)格式不正確募疮,則需要通過本地校驗后提示用戶當前信息格式不正確炫惩,請修改后再提交的場景。
說明:在上一例子中阿浓,用戶在頁面對話框中他嚷,輸入了手機號,但是位數(shù)少于11位芭毙,則需要通過Toast提示用戶筋蓖,手機號碼位數(shù)不正確,請檢查退敦。此時粘咖,我們通過Mock一個6位的字符串,通過check方法進行校驗和處理侈百。
代碼片段:
// viewModel 層代碼:
func check(person:Person)->(Result)
Unit Test代碼:
// Test 部分代碼:
let mockPerson = Person(phone:"123456", name:"Lei")
let result = viewModel.check(mockPerson)
expect(result).to(equal(Result.lessThan))
順便提一下瓮下,此場景也可以通過UI自動化測試來覆蓋:
// UITest 部分代碼:
func waitForElementToAppear(_ element: XCUIElement, timeout: TimeInterval = 5, file: String = #file, line: UInt = #line) {
let existsPredicate = NSPredicate(format: "exists == true")
expectation(for: existsPredicate,
evaluatedWith: element, handler: nil)
waitForExpectations(timeout: timeout) { (error) -> Void in
if (error != nil) {
let message = "Failed to find \(element) after \(timeout) seconds."
self.recordFailure(withDescription: message, inFile: file, atLine: Int(line), expected: true)
}
}
}
let tablesQuery = app.tables
tablesQuery.staticTexts["商家詳情"].tap()
let textField = app.textFields["phoneNumber"]
textField.tap()
textField.clearText(andReplaceWith: "123456")
app.staticTexts["提交"].tap()
let element = app.staticTexts["手機號碼位數(shù)不正確,請檢查"]
waitForElementToAppear(element, timeout: 10)
Stub
場景訴求:在業(yè)務場景中钝域,我們經(jīng)常需要根據(jù)某種操作的異常讽坏,通過UI頁面對用戶進行Toast提示,比如例证,我們期望在進行業(yè)務的提交處理時路呜,因為服務返回的特殊結果,需要通過UI層展示一個提示织咧。
說明:這是一個異常處理拣宰,需要通過ViewModel層的開發(fā)來實現(xiàn)異常展現(xiàn)的邏輯,通常的開發(fā)方法是在調(diào)用Service進行業(yè)務邏輯處理時烦感,通過BFF真是請求返回一個錯誤巡社,才能進行異常流程的開發(fā)和調(diào)試。而我們通過對Service層的Stub手趣,使其返回相應的異常結果晌该,ViewModel層只需要捕獲這些異常進行處理即可快速處理業(yè)務的分支邏輯肥荔。
代碼片段:
// 首先對 Service進行 Protocol 抽象:
protocol ServiceProtocol {
typealias successCallback = () -> Void
typealias failureCallback = (_ error: Error) -> Void
func checkoutService(_ cart: Dictionary<Food, Int>, success: @escaping successCallback, failure: @escaping failureCallback)
}
Unit Test代碼:
// 進行請求異常的Stub模擬,調(diào)用該實現(xiàn)時朝群,即返回一個返回錯誤的Stub
class StubServiceFail: ServiceProtocol {
var error = ResponseError()
// stub fail status
func checkoutService(_ cart: Dictionary<Food, Int>, success: @escaping successCallback, failure: @escaping failureCallback) {
failure(error)
}
}
// 進行驗證處理:
context("checkout") {
it("should be callback fail when call checkout service stub fail 9001") {
let stubService = StubServiceFail()
stubService.error = ResponseError(code: 9001, message: "no stock")
ViewModel.service = stubService
// 進行異常的驗證
waitUntil(timeout: .seconds(3)) { done in
foodListViewModel.checkout {
} fail: { error in
done()
}
}
}
}
結束語
以上說明和代碼片段燕耿,便是我對于測試替身在iOS編程開發(fā)中的一點點實踐和整理,現(xiàn)在依然記得姜胖,早年在單元測試照貓畫虎實踐Mock和Stub方法誉帅,再到后來引入BDD概念和各種測試框架,測試覆蓋率是上去了右莱,質(zhì)量也有可觀的收益了蚜锨,卻并沒有一個基礎的理論明確告訴你為什么這么做,哪種場景下應該這么做慢蜓。通過這次測試替身的實踐亚再,讓我明白了測試替身的基本概念,也明白了在什么場景下使用哪種測試方法更合適晨抡,希望這邊文章也能幫到迷惑的你氛悬。