測試替身在iOS開發(fā)中的實現(xiàn)整理

開始之前

請允許先介紹在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ì)量也有可觀的收益了蚜锨,卻并沒有一個基礎的理論明確告訴你為什么這么做,哪種場景下應該這么做慢蜓。通過這次測試替身的實踐亚再,讓我明白了測試替身的基本概念,也明白了在什么場景下使用哪種測試方法更合適晨抡,希望這邊文章也能幫到迷惑的你氛悬。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市耘柱,隨后出現(xiàn)的幾起案子如捅,更是在濱河造成了極大的恐慌,老刑警劉巖调煎,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伪朽,死亡現(xiàn)場離奇詭異,居然都是意外死亡汛蝙,警方通過查閱死者的電腦和手機烈涮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門伶授,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谁鳍,“玉大人消略,你說我怎么就攤上這事力图∏铉停” “怎么了槽片?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵灯抛,是天一觀的道長妇押。 經(jīng)常有香客問我需了,道長跳昼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任肋乍,我火速辦了婚禮鹅颊,結果婚禮上,老公的妹妹穿的比我還像新娘墓造。我一直安慰自己堪伍,他們只是感情好锚烦,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著帝雇,像睡著了一般涮俄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上尸闸,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天彻亲,我揣著相機與錄音,去河邊找鬼吮廉。 笑死苞尝,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的茧痕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼恼除,長吁一口氣:“原來是場噩夢啊……” “哼踪旷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起豁辉,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤令野,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后徽级,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體气破,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年餐抢,在試婚紗的時候發(fā)現(xiàn)自己被綠了现使。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡旷痕,死狀恐怖碳锈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情欺抗,我是刑警寧澤售碳,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站绞呈,受9級特大地震影響贸人,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜佃声,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一艺智、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧圾亏,春花似錦力惯、人聲如沸碗誉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哮缺。三九已至,卻和暖如春甲喝,著一層夾襖步出監(jiān)牢的瞬間尝苇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工埠胖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留糠溜,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓直撤,卻偏偏與公主長得像非竿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子谋竖,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

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

  • 1红柱、設計模式是什么? 你知道哪些設計模式蓖乘,并簡要敘述锤悄? 設計模式是一種編碼經(jīng)驗,就是用比較成熟的邏輯去處理某一種類...
    方白羽lw閱讀 811評論 0 0
  • 內(nèi)存中的區(qū)域劃分 棧區(qū)(stack):由系統(tǒng)自動分配和釋放嘉抒,存放局部變量的值零聚,容量小速度快,有序堆:一般由程序員分...
    switer_iOS閱讀 311評論 0 0
  • 設計模式是什么些侍? 你知道哪些設計模式隶症,并簡要敘述? 設計模式是一種編碼經(jīng)驗岗宣,就是用比較成熟的邏輯去處理某一種類型的...
    卑微的戲子閱讀 623評論 0 1
  • (答案不唯一沿腰,僅供參考,文章最后有福利)目錄 一狈定、基礎知識點 設計模式是什么颂龙? 你知道哪些設計模式,并簡要敘述纽什?設...
    ios南方閱讀 6,405評論 0 11
  • 一措嵌、Java基礎 1、Java中兩種數(shù)據(jù)類型(為后面進一步提問做鋪墊) (1)基本數(shù)據(jù)類型芦缰,分為boolean企巢、b...
    編程俠Java閱讀 932評論 0 13