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

开始之前

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

  • 在iOS开发的过程中,我们常接触到的单元测试框架有 Qucik以及他的好朋友Nimble,前者是iOS编程开发中行为驱动开发框架,后者是对iOS平台XCTest结果预期处理的更简易化、人性化的封装。

  • iOS的UI自动化测试,则直接使用的是XCTest框架,一方面是很容易进行脚本的录制,另一方面可以通过WebDriverAgent等三方框架接入,结合Appium以及行为描述语言Cucumber等,实现多语言跨端的脚本化的自动化测试,此处按住不表。


再来说说测试替身(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.

实战

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

  • 该 iOS App Swift 语言开发,使用MVVM架构,

  • 通过CocoaPods进行依赖管理,同时集成了以下三方组件:
    测试组件:Quick和Nimble,
    弹窗组件:Toast
    网络基础组件:Alamofire
    以及服务模拟组件:OHHTTPStubs/Swift


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概念和各种测试框架,测试覆盖率是上去了,质量也有可观的收益了,却并没有一个基础的理论明确告诉你为什么这么做,哪种场景下应该这么做。通过这次测试替身的实践,让我明白了测试替身的基本概念,也明白了在什么场景下使用哪种测试方法更合适,希望这边文章也能帮到迷惑的你。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 1、设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类...
    方白羽lw阅读 797评论 0 0
  • 内存中的区域划分 栈区(stack):由系统自动分配和释放,存放局部变量的值,容量小速度快,有序堆:一般由程序员分...
    switer_iOS阅读 307评论 0 0
  • 设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的...
    卑微的戏子阅读 619评论 0 1
  • (答案不唯一,仅供参考,文章最后有福利)目录 一、基础知识点 设计模式是什么? 你知道哪些设计模式,并简要叙述?设...
    ios南方阅读 6,386评论 0 11
  • 一、Java基础 1、Java中两种数据类型(为后面进一步提问做铺垫) (1)基本数据类型,分为boolean、b...
    编程侠Java阅读 924评论 0 13