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

開始之前

請(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è)試方法更合適,希望這邊文章也能幫到迷惑的你。

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

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

  • 1、設(shè)計(jì)模式是什么? 你知道哪些設(shè)計(jì)模式,并簡要敘述? 設(shè)計(jì)模式是一種編碼經(jīng)驗(yàn),就是用比較成熟的邏輯去處理某一種類...
    方白羽lw閱讀 870評(píng)論 0 0
  • 內(nèi)存中的區(qū)域劃分 棧區(qū)(stack):由系統(tǒng)自動(dòng)分配和釋放,存放局部變量的值,容量小速度快,有序堆:一般由程序員分...
    switer_iOS閱讀 529評(píng)論 0 0
  • 設(shè)計(jì)模式是什么? 你知道哪些設(shè)計(jì)模式,并簡要敘述? 設(shè)計(jì)模式是一種編碼經(jīng)驗(yàn),就是用比較成熟的邏輯去處理某一種類型的...
    卑微的戲子閱讀 681評(píng)論 0 1
  • (答案不唯一,僅供參考,文章最后有福利)目錄 一、基礎(chǔ)知識(shí)點(diǎn) 設(shè)計(jì)模式是什么? 你知道哪些設(shè)計(jì)模式,并簡要敘述?設(shè)...
    ios南方閱讀 6,696評(píng)論 0 12
  • 一、Java基礎(chǔ) 1、Java中兩種數(shù)據(jù)類型(為后面進(jìn)一步提問做鋪墊) (1)基本數(shù)據(jù)類型,分為boolean、b...
    編程俠Java閱讀 1,057評(píng)論 0 13

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