開始之前
請(qǐng)?jiān)试S先介紹在iOS開發(fā)測(cè)試中的一些基礎(chǔ)框架和理論:
在iOS開發(fā)的過程中,我們常接觸到的單元測(cè)試框架有 Qucik以及他的好朋友Nimble,前者是iOS編程開發(fā)中行為驅(qū)動(dòng)開發(fā)框架,后者是對(duì)iOS平臺(tái)XCTest結(jié)果預(yù)期處理的更簡易化、人性化的封裝。
iOS的UI自動(dòng)化測(cè)試,則直接使用的是XCTest框架,一方面是很容易進(jìn)行腳本的錄制,另一方面可以通過WebDriverAgent等三方框架接入,結(jié)合Appium以及行為描述語言Cucumber等,實(shí)現(xiàn)多語言跨端的腳本化的自動(dòng)化測(cè)試,此處按住不表。
再來說說測(cè)試替身(Test Double),為了避免爭議,下面上Martin Fowler對(duì)于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.
實(shí)戰(zhàn)
有了以上的基礎(chǔ)理論后,我們來逐條看這些方式在iOS編程中是如何實(shí)現(xiàn)的,先做工程架構(gòu)假設(shè):
該 iOS App Swift 語言開發(fā),使用MVVM架構(gòu),
通過CocoaPods進(jìn)行依賴管理,同時(shí)集成了以下三方組件:
測(cè)試組件:Quick和Nimble,
彈窗組件:Toast
網(wǎng)絡(luò)基礎(chǔ)組件:Alamofire
以及服務(wù)模擬組件:OHHTTPStubs/Swift
Dummy
場(chǎng)景訴求: 我有一個(gè)頁面,布局了一個(gè)界面元素以及一個(gè)提交按鈕, 為了驗(yàn)證該頁面的元素是否在頁面初始化后正常加載,我需要通過UI自動(dòng)化測(cè)試來運(yùn)行工程,并在App啟動(dòng)后,通過腳本錄制進(jìn)入到該頁面,并進(jìn)行頁面元素的檢查驗(yàn)證。(此處只做元素是否正常顯示的驗(yàn)證)。
說明: 因?yàn)槭褂肕VVM結(jié)構(gòu),在頁面進(jìn)行初始化的時(shí)候,需要進(jìn)行ViewModel的初始化,很明顯,在我們通過StoryBoard托拉拽期間,ViewModel是不參與邏輯的,但因?yàn)樵诔跏蓟疺C的時(shí)候,就需要將ViewModel綁定到VC,所以viewModel需要一個(gè)初始值來保證代碼能夠正常運(yùn)行但是不參與邏輯模塊。
代碼片段:
// 初始化ViewModel
let dummyViewModel = ViewModel()
// 將其作為參數(shù)參與到ViewController的創(chuàng)建中
let viewController = ViewController(viewModel:dummyViewModel)
navgationController.push(viewController)
測(cè)試代碼:
// UITest中對(duì)于button是否顯示的判斷
let app = XCUIApplication()
app.launch()
let tablesQuery = app.tables
tablesQuery.staticTexts["商家詳情"].tap()
let trackLabel = app.staticTexts["提交"]
XCTAssertEqual(trackLabel.exists, true)
Fake
場(chǎng)景訴求:在真是的開發(fā)場(chǎng)景中,針對(duì)于前端一般都會(huì)有配套BFF服務(wù),那么在開發(fā)的過程中,往往因?yàn)榉?wù)端開發(fā)與前端開發(fā)的進(jìn)度不同步,會(huì)出現(xiàn)前端開發(fā)同學(xué)需要通過一種輕量級(jí)的實(shí)現(xiàn)來替代后端BFF,以滿足其開發(fā)階段模擬服務(wù)數(shù)據(jù)達(dá)到實(shí)現(xiàn)業(yè)務(wù)訴求的情況。
說明:在上一例子中,我們?cè)夙撁胬镞x擇了幾個(gè)checklist選項(xiàng) ,并點(diǎn)擊提交按鈕,此時(shí)需要調(diào)用API服務(wù)發(fā)起訂單提交請(qǐng)求,此時(shí)會(huì)有這樣一個(gè)場(chǎng)景:提交成功。假設(shè)我們與后端開發(fā)已經(jīng)進(jìn)行了接口API約定,定義了正常處理的返回?cái)?shù)據(jù)結(jié)構(gòu),則可以通過啟用一個(gè)輕量級(jí)實(shí)現(xiàn)的MockServer,返回特定結(jié)果,幫助我們完成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)
}
}
測(cè)試代碼:
// Test 部分代碼:
let service = CheckoutService()
context("checkout") {
// 工序X fake BFF,實(shí)現(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結(jié)果,并將成功的結(jié)果通過接口返回
return fixture(filePath: stubPath!, status: 200, headers: ["Content-Type": "application/json"])
}
waitUntil(timeout: .seconds(5)) { done in
// 在service中進(jìn)行 checkout 服務(wù)調(diào)用,并等待5秒等待成功的返回結(jié)果。
service.checkout(Dictionary<Food, Int>()) {
done()
} failure: { error in
}
}
}
Mock
場(chǎng)景訴求:在業(yè)務(wù)場(chǎng)景中,我們經(jīng)常需要根據(jù)某種操作的異常case,通過UI頁面對(duì)用戶進(jìn)行Toast提示,比如,在進(jìn)行業(yè)務(wù)的提交處理時(shí),因?yàn)閿?shù)據(jù)格式不正確,則需要通過本地校驗(yàn)后提示用戶當(dāng)前信息格式不正確,請(qǐng)修改后再提交的場(chǎng)景。
說明:在上一例子中,用戶在頁面對(duì)話框中,輸入了手機(jī)號(hào),但是位數(shù)少于11位,則需要通過Toast提示用戶,手機(jī)號(hào)碼位數(shù)不正確,請(qǐng)檢查。此時(shí),我們通過Mock一個(gè)6位的字符串,通過check方法進(jìn)行校驗(yàn)和處理。
代碼片段:
// 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))
順便提一下,此場(chǎng)景也可以通過UI自動(dòng)化測(cè)試來覆蓋:
// 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["手機(jī)號(hào)碼位數(shù)不正確,請(qǐng)檢查"]
waitForElementToAppear(element, timeout: 10)
Stub
場(chǎng)景訴求:在業(yè)務(wù)場(chǎng)景中,我們經(jīng)常需要根據(jù)某種操作的異常,通過UI頁面對(duì)用戶進(jìn)行Toast提示,比如,我們期望在進(jìn)行業(yè)務(wù)的提交處理時(shí),因?yàn)榉?wù)返回的特殊結(jié)果,需要通過UI層展示一個(gè)提示。
說明:這是一個(gè)異常處理,需要通過ViewModel層的開發(fā)來實(shí)現(xiàn)異常展現(xiàn)的邏輯,通常的開發(fā)方法是在調(diào)用Service進(jìn)行業(yè)務(wù)邏輯處理時(shí),通過BFF真是請(qǐng)求返回一個(gè)錯(cuò)誤,才能進(jìn)行異常流程的開發(fā)和調(diào)試。而我們通過對(duì)Service層的Stub,使其返回相應(yīng)的異常結(jié)果,ViewModel層只需要捕獲這些異常進(jìn)行處理即可快速處理業(yè)務(wù)的分支邏輯。
代碼片段:
// 首先對(duì) Service進(jìn)行 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代碼:
// 進(jìn)行請(qǐng)求異常的Stub模擬,調(diào)用該實(shí)現(xiàn)時(shí),即返回一個(gè)返回錯(cuò)誤的Stub
class StubServiceFail: ServiceProtocol {
var error = ResponseError()
// stub fail status
func checkoutService(_ cart: Dictionary<Food, Int>, success: @escaping successCallback, failure: @escaping failureCallback) {
failure(error)
}
}
// 進(jìn)行驗(yàn)證處理:
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
// 進(jìn)行異常的驗(yàn)證
waitUntil(timeout: .seconds(3)) { done in
foodListViewModel.checkout {
} fail: { error in
done()
}
}
}
}
結(jié)束語
以上說明和代碼片段,便是我對(duì)于測(cè)試替身在iOS編程開發(fā)中的一點(diǎn)點(diǎn)實(shí)踐和整理,現(xiàn)在依然記得,早年在單元測(cè)試照貓畫虎實(shí)踐Mock和Stub方法,再到后來引入BDD概念和各種測(cè)試框架,測(cè)試覆蓋率是上去了,質(zhì)量也有可觀的收益了,卻并沒有一個(gè)基礎(chǔ)的理論明確告訴你為什么這么做,哪種場(chǎng)景下應(yīng)該這么做。通過這次測(cè)試替身的實(shí)踐,讓我明白了測(cè)試替身的基本概念,也明白了在什么場(chǎng)景下使用哪種測(cè)試方法更合適,希望這邊文章也能幫到迷惑的你。