lsekfe 发表于 2021-6-25 10:13:34

单元测试与测试的代码覆盖率之间的关联

 我一直认为代码覆盖率是最形式主义的技术工具,覆盖率再高也不能保证代码本身无懈可击,该出 Bug 的地方 100% 的覆盖率也救不了你。
  其实作为一种辅助度量工具,代码覆盖本身并没有什么错,有位仁兄说得好:“在追求精益求精的道路上,我们应该无所不用其极”。错就错在拿代码覆盖率当考核指标,以此来衡量测试人员的工作水平,对此我相当无语,也相当反感。
  有识之士一定会说:你也不要以偏概全,路径覆盖所度量出的代码覆盖率还是相当靠谱的嘛。
  简单普及下,代码覆盖算法有很多种,大致上对比准确性:路径覆盖 > 条件覆盖 ~= 判定覆盖 > 语句覆盖。而且这只是说条件分支,循环什么的还有别的算法就不多说了。这些算法在覆盖率都达到 100% 的前提下,其“靠谱”程度可能有天壤之别。问题就出在下决策使用代码覆盖率做考核的人往往不明白这种差别,这就给了落地执行的人可趁之机,很容易就演变成了“在追求 100% 代码覆盖率的道路上,我们应该无所不用其极“。若是连落地执行人都不懂,那就更悲剧了,一群人对着水份极大的 100% 乐得嘴都合不拢,想想都难受。
  所以对于代码覆盖率的不当应用,只会让大家越走越偏,浪费时间不说收效还甚微;反过来恰当的使用代码覆盖率又对团队的要求极高,只有一个人懂行是不够的,因为你没有那么多时间精力去检查结果是不是真的靠谱。如果每一个人都按照靠谱的方式去写代码和测试,不用测试覆盖率也没什么大不了的。因此如果我是初创团队的负责人,我宁可选择把时间和精力放在测试用例本身上,测试本身靠谱了,测试覆盖率的辅助价值才能靠谱。
  测试中的“伪装术”
  前面我多次提到了测试用例中的“伪造”技术(或者是“伪装”技术),对于初学者来说一定觉得很困惑吧?这是一种编写测试的进阶技巧,说是进阶但其实它并不难,只是一来概念上比较抽象,理解它需要花点心思,二来“伪造”技术流派甚多,极易让人混淆接着望而生畏,以至于知道也不敢大胆使用。在此我借机会谈谈我的理解和心得,望能助你解惑。
  由于不同的语言不同的平台有自己的发展历史和文化背景,所以会出现相似甚至相同的概念却有着不同的称谓的现象,测试中的“伪装术”就是其中的典型代表,而且它的情况更复杂,很多名词术语都和测试中的“伪装术”有关系,它们有:fakes, doubles, stubs, mocks, spies……说不定还有更多我不知道的!
  顺便一提,它们的动名词形式,如 mocking,代表一种“伪装”的技术;而它们的名词形式,如 mocks 代表应用了此种“伪装”之后的产物。
  它们的复杂性在于:基本上你可以认为它们是一回事儿,但是在某些细节上各有不同。这就好比老王家生了五个儿子,他们都是老王家的儿子,但是他们各有各的特点;对外人来说(较高的抽象层面上),知道他们都是老王家的儿子就够了,但对老王自家人来说(更具体的层面),就可以分辨出各个的不同来了。
  它们的复杂性还在于:不同的语言平台或社区里,它们中的一些常常是等价的,甚至其中的一些干脆就完全不提(这种界限上的模糊往往来自于语言自身的特性,比如说动态语言相对于静态语言更加灵活,往往没必要在对象的类型上太较真)。
  有鉴于此,我没打算逐个详细的介绍,这太困难了!我只介绍其概念和基本用法,在界限足够分明的时候再提及它们的名字。
  OK,什么是测试伪装术?
  你特别喜欢下象棋,不过下棋这事儿一个人是做不了的,所以你最喜欢的是和老王一起下象棋,你俩棋逢对手将遇良材脾气也对味,一起下棋最是过瘾(外部依赖需求满足条件),这要是换了别人,下棋没了乐趣不说还说不定得打起来(外部依赖需求不满足条件)。
  有一天半夜,你梦见经一老神仙授艺,你的棋力大涨三段。这一乐就给醒了,棋瘾犯了怎么也睡不着,就像找个对手来一较高低(待测目标)。但找谁呢?这么晚了也不好把老王从被窝里拽出来吧?(单元测试的隔离特性,源自于外部依赖的不确定性)你灵机一动,用电脑模拟了一个老王出来(测试伪装术登场),对于此时的你来说,他是不是真的老王并不重要,重要的是“他”能满足你的要求。
  伪装对象
  如上所述,你用电脑模拟的老王就是一个伪装对象了——一个用伪装术创造出来的对象。本质上它就是一个普通的对象,甚至可以手工去创造它。但是为了方便起见,我们往往会使用一些专用的伪装库来生成伪装对象,这些伪装库为我们提供了一些方法来协助我们仿造对象的属性和行为以满足测试用例的各种需求。重要的是,伪装对象和真实的外部依赖对象一点关系都没有,这就使得我们的单元测试可以在完全隔离外部依赖的前提下运行生效,因此即使实际代码里没有任何外部依赖存在的痕迹,我们依旧可以在假设外部依赖存在的前提下完成我们的目标代码实现。
  伪装对象,有时候称之为 Faked Object 或 Fakes,也有称之为 Doubled Object 或 Doubles 的,并且不同的语言环境下对于伪装对象的严格定义也会有差别,不过其原理都是一致的。以下我们统一用 double 来称呼伪装对象。
  还有一种分类法是把所有类型的伪装对象统一称作 doubles,然后再细分类型,这样分下来,dummies,fakes,stubs,mocks,spies 这些是子类型,各有各的特点。
  本文不做学术研讨之用,你喜欢那一种就用那一种好了,无所谓的。
  伪装对象可以和外部依赖一模一样,也可以完全不同,这个差别取决于你的测试用例到底依赖它多少。根据测试用例来塑造伪装对象的特性(可以理解为仿真度),由此而发展出各种伪装技术来。
  伪装对象的类型匹配
  你模拟了一个老王还不够,还希望这个“假老王”可以和真老王一样的五官眉眼,胖瘦高低,这样你才有身临其境的感觉。
  这就是对外部依赖有类型匹配要求,需要模拟其属性了。模拟属性不是难事,但是能否严格的满足类型匹配要求往往要视语言环境和测试工具的能力而定。我用 RSpec 写几个简单的例子(题主请原谅我不懂 PHP):
wang = double()
  # or
  wang = double('player')
这个 wang 就是个 double 对象,什么类型不关心。
 wang = instance_double('Player')这个 wang 就不一样了,它一定是 Player 类的实例,如果 Player 类不存在则测试抛出错误。
wang = instance_double('Player', name: 'Wang')
  expect(wang.name).to equal('Wang')这是模拟属性的例子,很直白(为节省篇幅只写关键代码)。顺便一提,如果一个 double 带有“预置”的返回值用以响应对特定方法的调用,我们就叫它 stub,响应的其伪装技术就叫 stubbing。比如本例中,创建 wang 这个 double 的时候就为其指明了 name 属性,于是断言里 wang.name 调用时就会返回预先定义好的 Wang。这个例子可以写的更复杂(但更容易理解)些:
 wang = instance_double('Player')
  allow(wang).to receive(:name).and_return('Wang')
  expect(wang.name).to equal('Wang')如果你英语还行,你会发现上例读起来相当流畅!试着翻译一次:
wang 是 Player 的实例 double
  允许 wang 接受消息(即方法调用):name 并且返回字符串 Wang
  期望 wang.name 的返回结果等于字符串 Wang这样的代码风格是典型的 BDD 式的代码风格,BDD 需要对代码的行为进行准确描述,因此基本上都会创造一些可读性非常好的 DSL(领域特定语言)来帮助你编写测试。不过这也不是绝对的就是了。
  伪装对象的行为匹配
  在你看来,仅仅看着像老王还不足够,你更需要它能够像老王一样的下棋
  比如说老王很喜欢用炮,所以一旦丢了炮,他会很恼火,还会耍赖悔棋……
  更进一步的,我们不但希望模拟外部依赖的类型和属性,还希望模拟它们的行为,特别是和待测目标交互时产生的行为。这样的测试如果不加以隔离将会产生至少两种后果:
  1、如果外部依赖不存在,则测试肯定无法通过(所以才要伪装术)
  2、如果外部依赖变更,则会导致测试失败。严格来说这种后果不是测试的责任,外部依赖的变更应该保持外部接口不变和返回结果不变,只变更内部的行为。使用伪装术的好处就在于一旦出现这种情况不至于让你误以为是己方的代码除了问题。
  当然你也会想,如果用了伪装对象,那么外部依赖变了己方的测试还浑然不知,这不是很危险吗?有道理,不过单元测试的职责是测试己方代码的正确性,对于外部依赖的模拟不一定非得和模拟对象完全一致,真实的交互应该先由集成测试来捕捉问题,否则很容易迷失在复杂的代码交互之中。
 wang = double(name: 'Wang').as_null_object
  cannon = double(side: :black, ...)
  expect(wang).to receive(:ask_for_takeback).with(cannon).at_least(:once)
  me.taking(cannon)你会注意到,这里是先断言老王会要求悔棋(至少要求一次),然后才是吃子。这就好像一个触发器,你先宣称依赖对象在你做出特定行为后肯定会表现出对应的行为,之后你做一下试试看是不是会发生。
  这种先断言后调用的伪装技术称之为 mocking,有“糊弄人”的意思在里面(可不是么!),它还有一个比较学术化的正统称谓,叫做 Message expectations。也有一些程序员不喜欢这种风格,他们觉得先调用后断言才更合乎常理,于是就有了非常类似的 spies,如下:
me.taking(cannon)
  expect(wang).to have_received(:ask_for_takeback).with(cannon).at_least(:once)上述介绍皆以 RSpec3 为参考测试框架,或许对于其他语言的其他框架来说一些概念和用法会大相径庭,这我真说不好,所以参考为主,不必尽信了。重要的是了解这些技术到底是干啥的,对编写测试有啥帮助,具体的技术细节还是要以你使用的工具的文档为准哦。





页: [1]
查看完整版本: 单元测试与测试的代码覆盖率之间的关联