测试替身在iOS开发中的实现整理

2022-06-01  本文已影响0人  leisurehuang

开始之前

请允许先介绍在iOS开发测试中的一些基础框架和理论:


再来说说测试替身(Test Double),为了避免争议,下面上Martin Fowler对于Test Double解释。

实战

有了以上的基础理论后,我们来逐条看这些方式在iOS编程中是如何实现的,先做工程架构假设:


Dummy

场景诉求: 我有一个页面,布局了一个界面元素以及一个提交按钮, 为了验证该页面的元素是否在页面初始化后正常加载,我需要通过UI自动化测试来运行工程,并在App启动后,通过脚本录制进入到该页面,并进行页面元素的检查验证。(此处只做元素是否正常显示的验证)。
说明: 因为使用MVVM结构,在页面进行初始化的时候,需要进行ViewModel的初始化,很明显,在我们通过StoryBoard托拉拽期间,ViewModel是不参与逻辑的,但因为在初始化VC的时候,就需要将ViewModel绑定到VC,所以viewModel需要一个初始值来保证代码能够正常运行但是不参与逻辑模块。

代码片段:

// 初始化ViewModel
let dummyViewModel = ViewModel()
// 将其作为参数参与到ViewController的创建中 
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

场景诉求:在真是的开发场景中,针对于前端一般都会有配套BFF服务,那么在开发的过程中,往往因为服务端开发与前端开发的进度不同步,会出现前端开发同学需要通过一种轻量级的实现来替代后端BFF,以满足其开发阶段模拟服务数据达到实现业务诉求的情况。
说明:在上一例子中,我们再页面里选择了几个checklist选项 ,并点击提交按钮,此时需要调用API服务发起订单提交请求,此时会有这样一个场景:提交成功。假设我们与后端开发已经进行了接口API约定,定义了正常处理的返回数据结构,则可以通过启用一个轻量级实现的MockServer,返回特定结果,帮助我们完成Service层的逻辑开发。

代码片段:

// 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,实现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 服务调用,并等待5秒等待成功的返回结果。
            service.checkout(Dictionary<Food, Int>()) {
                done()
            } failure: { error in

            }
        }
    }


Mock

场景诉求:在业务场景中,我们经常需要根据某种操作的异常case,通过UI页面对用户进行Toast提示,比如,在进行业务的提交处理时,因为数据格式不正确,则需要通过本地校验后提示用户当前信息格式不正确,请修改后再提交的场景。
说明:在上一例子中,用户在页面对话框中,输入了手机号,但是位数少于11位,则需要通过Toast提示用户,手机号码位数不正确,请检查。此时,我们通过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["手机号码位数不正确,请检查"]
waitForElementToAppear(element, timeout: 10)


Stub

场景诉求:在业务场景中,我们经常需要根据某种操作的异常,通过UI页面对用户进行Toast提示,比如,我们期望在进行业务的提交处理时,因为服务返回的特殊结果,需要通过UI层展示一个提示。
说明:这是一个异常处理,需要通过ViewModel层的开发来实现异常展现的逻辑,通常的开发方法是在调用Service进行业务逻辑处理时,通过BFF真是请求返回一个错误,才能进行异常流程的开发和调试。而我们通过对Service层的Stub,使其返回相应的异常结果,ViewModel层只需要捕获这些异常进行处理即可快速处理业务的分支逻辑。

代码片段:

// 首先对 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模拟,调用该实现时,即返回一个返回错误的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编程开发中的一点点实践和整理,现在依然记得,早年在单元测试照猫画虎实践Mock和Stub方法,再到后来引入BDD概念和各种测试框架,测试覆盖率是上去了,质量也有可观的收益了,却并没有一个基础的理论明确告诉你为什么这么做,哪种场景下应该这么做。

上一篇下一篇

猜你喜欢

热点阅读