测试篇3——敏捷开发模式下的测试又是怎么失败的?

能力、能力、能力。重要的事情一定要说三遍!

既然已经有那么多书告诉我们敏捷测试应该怎么做,那为什么还有那么多团队(我看到的至少一半以上转型到敏捷开发模式的团队)无法做好测试呢?或者说并没有感受到测试效果有明显的提升?

答案只可能有一个——我们虽然知道该怎么做,但我们并没有做到位,准确的说是我们缺少做到位的能力!我知道你看到这里时内心深处是不屑的,心里一定在想自己的能力肯定是OK的,问题就在这,我们缺少做到位的能力,但同时我们不承认这一点所以我们又进一步失去了提升能力的可能。

我们可以尝试从不同类型测试的角度稍作回顾。

单元测试

内心其实是拒绝的

即使团队做了敏捷转型,还是有很多开发人员拒绝写单元测试,原因无非有下面几种:

  1. 难写难调,耗时太长,写业务代码就半天时间,写单元测试可能要两天

  2. 方案稍做修改,单元测试就废了,需要重新写一遍

当然还有一部分开发人员纯粹就是不想做“多余的事”,只要我交付了功能,你管我写单元测试没有,写代码对这部分开发人员来说可能只是混口饭吃而已,人各有志,这里就不讨论了。

不管什么单元测试框架通常会有一定的学习成本,特对第一次接触xUnit框架时需要花一段时间才能适应一些奇怪的语法。刚刚接触单元测试的开发人员有很多会认为效率低,成本太高,这其实是一个很大的误解,认为开发人员的工作就是产出代码,而准确的说法是交付可供用户使用的功能,在这个过程中代码编写所占的工作量只是很小的部分,还包括用户需求沟通和理解,如何转换为软件需求,方案设计,编码,调试,测试,验收,最终交付,在编码环境为了提升效率而舍弃单元测试,就好比上山砍柴却不想磨刀,在这个环节节省的时间将来会在调试,测试,验收甚至是交付之后加倍的浪费出去。如果我们观察一个项目里面要求开发人员提供每日联调报告的话,那基本可以断定这个项目在单元测试这个环节偷懒了,因为具备充分单元测试的代码,在联调阶段的效率是非常高的。

对于第二种,其实反映的是代码接口设计的不合理,也可能是逻辑分层有问题,也可能是耦合太紧,开发人员发现在这种问题其实是回顾和提升自身设计能力的好机会,一旦我们停止抱怨,以积极的态度来面对,认真分析找到根因所在,反复去打磨我们的代码,就能够脱胎换骨,破茧而出!

写不出好的单元测试

说到单元测试不得不说说TDD,我个人评价单元测试好坏的标准是

优秀的单元测试 = 以TDD的方式写出来的单元测试

这句话这么说也成立:

先写代码后补单元测试 = 一般的单元测试

很多开发人员度过了第一关,在代码开发完成之后开始尝试编写一些单元测试,但是绝大多数会止步于TDD,原因还是一样的,成本太高……

如果你有过先写代码后写UT的经验,下面这几种情形多少会经历过:

  • 测试的有效性很难保证,有时候代码有bug,跟着编写单元测试的时候写是错的,等到后面联调或者集成测试才发现

  • 用例很长,写100行的业务代码,对应要写上500行的单元测试代码,而且总感觉还有什么场景没有考虑到

  • 当行覆盖率达到85%以上之后,再想往上提升就变得异常困难

  • 用例调试起来困难,细节很多,一旦用例失败,可能需要花很长时间才能修复

  • 看半个月之前写的用例,已经完全不知道写的是啥

在网上经常看到开发人员对代码编辑工具好坏的讨论,Vim党在这个话题上表现非常活跃,据说只要坚持几周使用Vim你就会对所有其他工具嗤之以鼻,然而真正能坚持下来的不多,TDD大概也是一样,等你真正掌握TDD的技巧之后,如果你不用TDD的方式开发反而会觉得各种别扭,可惜的是能再往前迈出一步,看到这种风景的开发人员是少之又少,以个人经验来看,这个比例大概只有5%!

什么叫好的单元测试

说到这个话题,首先要排除的一种是没有ASSERT的测试用例,这种用例很容易编写,也能达到很高的行覆盖率,然而完全没有任何价值,会编写这种用例的开发人员最好是早点识别出来并请出团队(如果你能做到的话)。

对于面向对象的语言,单元测试的测试对象就是一个具体的实现类,在TDD时我们先设计一个最正常的流程用例,并编写代码使用例通过,然后再识别这个用例中所有正交的外部依赖(比如入参、依赖的外部接口),然后以第一个用例为基准每次变化一个维度来进行用例设计。

比如测试对象类A依赖3个接口分别为iB,iC ,iD,每个接口再调用时分别有 2种,2种,3种返回值,那么虽然总共的业务分支有 223=12 种,但实际上以正交方式来设计用例时我们只需要 2+2+3-2 = 5个,即足以驱动我们编写出所有分支的代码了!以一个表格的方式来表示:

iB iC iD
1 1 1
2 1 1
1 2 1
1 1 2
1 1 3

如果你不用TDD而是采用后补的方式来设计用例,则很难体会到这种正交设计所带来的安全感,即使你把12种业务分支全部组合测试一遍,很可能还是会担心漏掉了什么场景,而且这些用例看起来也无法体现思路的连续性,起可读性也不高。而且,如果你不曾体会到TDD的3步军规“编写一个失败的用例”-》“编写代码让这个用例通过”-》“重构”,就很难理解为什么只需要5个用例就能覆盖所有的场景了,因为你增加除这5个用例之外的其他用例,你会发现直接就运行通过了,按照TDD的看法,这种用例没有存在的价值。

功能测试

缺少用户思维和用户沟通能力

一般来说功能测试框架更容易掌握一点,比如Robot Framework这种基于关键字的测试框架,有一定经验的开发人员只需要2-3天就能基本掌握。功能测试的难点在于用户思维,从用户的角度出发,那些场景是最常用的场景,那些场景是风险最大的场景,我们需要对这些场景进行功能测试用例的设计。

在设计功能测试用例时,很有可能某些测试场景和单元测试有一定的重复,但是没有关系,毕竟这是从两个不同的视角来设计的用例,代码内部设计可能将来会重构,单元测试也会相应修改,但只要功能不变其功能测试就不用做任何修改。

在实际开发功能测试用例时,开发人员包括QA经常很难从用户角度来对功能测试用例进行识别,原因在于开发人员看到的是由PO翻译过来的软件需求,而不是原始的用户需求,要理解原始的用户需求就需要开发人员和PO甚至是需求客户深入的沟通,然而实际情况,我看到的大多数开发人员甚至是QA都不擅长这种沟通,这导致在功能测试设计时就会很迷惑。

不愿意隔离外部依赖

功能测试的另一个难点是对外部依赖的隔离,在单元测试我们经常使用Mock技术,在功能测试阶段则经常使用打桩(Stub)技术,比如说依赖一个外部模块(比如需要和这个外部模块进行消息的交互),在功能测试时,正确的做法是编写这个外部依赖的桩实现,在功能测试用例中通过模拟这个这个外部模块的不同返回值来执行不同场景的测试。

而实际开发时,大多数开发人员并不愿意开发这个桩实现,既然需要跑一个外部依赖,那就把真实的外部依赖跑起来呗,这看起来是一个低成本的解决方案,然而我们要意识到这个外部依赖可能也还在开发中,其实现并不可靠,甚至进度处于落后状态,如果只有一个外部依赖的话风险可能还处于可控状态,而真实场景下外部依赖可能有多个,这些外部依赖中只要一个掉链子那功能测试就不能正常运行。做个简单的概率计算,假设外部依赖还是3个,每个外部依赖正常执行的概率是80%(看起来还可以接受),最终测试用例能正常执行的概率就是80%80%80%=50%。这也是我们经常看到很多团队功能测试执行成功率非常低的原因之一。

自动化的泥潭

这个话题和上个话题其实是同一个,在功能测试中如果不能将外部依赖隔离开来,我们在实施自动化的时候就会陷入困境。

自动化测试给我们提供了一个非常及时的反馈机制,当我们提交新代码之后,如果 CI 变成了红色,意味着新提交的代码有问题,我们需要及时修复或者回退,以保证在最短的时间里将 CI 恢复。而如果在自动化测试执行时依赖了太多外部不稳定的因素,比如说网络、数据库、其他处于开发阶段的组件,就会带来下面这些问题:

  • 自动化环境难以维护,搭建环境很困难,一旦环境出现出现问题要定位和修复的周期长
  • 自动化测试运行速度慢,比如需要半天才能执行一次,上午提交的代码可能需要等到下班才能看到影响
  • 自动化测试执行通过率低,这个原因在上面已经说明,CI 长期处于红色会让开发人员无法得到及时反馈,丧失 CI 的价值
  • 各组件团队之间互相扯皮,推诿责任,助长不好的开发文化形成

探索性测试

测试的直觉

探索性测试的能力的高低基本可以区分一个QA到底是优秀还是普通,因为探索性测试有点类似武侠小说中的“无招胜有招”,在单元测试中,我们可以识别测试对象的外部依赖并对其进行正交设计,在功能测试中只要我们能和用户建立有效的沟通,将用户需求理解透彻,要梳理出测试重点和风险也不难,但是在探索性测试中,更加强调测试人员的直觉(个人更喜欢灵性这个词)。

直觉不是凭空出现的,需要通过大量甚至反复的练习来积累,是建立在对系统非常深入的了解之上。打个比方,在警察系统中,优秀的探员能敏锐的抓住在其他探员眼中非常不起眼的线索并破解案件,如果问这位优秀的探员为什么是他而不是其他人注意到这条线索,他往往自己也不清楚,只能说“我就是知道”。

在进行探索性测试时,我们选择某一个“感觉有问题”的点,选择不同的输入来进行测试,在测试的同时仔细观察系统的反应,不遗巨细,认真思考,并根据结果和我们的经验来调整下一步的测试动作,或选用更合适的工具,或找开发人员就某个不确切的细节进行讨论,一次卓有成效的探索性测试应该能让测试人员进入“心流”的状态,在测试完成之后收获满满的成就感。

如果我们回到瀑布式开发的年代,在版本发布前的一两周我们会集中力量来进行测试,而且经常是人肉测试,其实就有很多次,我们也曾经没有意识的进入了这种“心流”的状态,只是我们不知道我们正在执行探索性测试。

自动化的神话

传统团队转型敏捷后,第一步要做的事情就是测试自动化和持续集成,在一个从没做过自动化测试的团队中引入这项实践会给所有人包括项目经理带来显而易见的收获以及心灵上巨大的震撼。

很多团队和项目都会度量测试用例的自动化比例来衡量当前测试工作的状态,追求100%的测试自动化。这个指标没错,我们应该自动化所有能自动化的测试用例,问题在于自动化测试同样不是银弹,如果在我们的组织中过度强调自动化的重要性,就会带来一个错觉——所有手工的测试都是错误的!这种错觉一旦形成,探索性测试就会消失,至少在这个领域我们反而会没有瀑布式开发做的更好。

当我们看到团队已经具备了上千的自动化用例,但还是在演示会或者系统测试环节,甚至是随手一点的时候出现了我们预期之外的结果,那就要小心我们是不是在探索性测试上疏忽了……

压力测试

压力测试或者性能测试的重要性不言而喻,不管是瀑布式的还是敏捷的,压力测试相信没有谁敢跳过,但压力测试的环境搭建起来比较麻烦,执行一次所需要的时间也比较长,所以一般来说执行的频率不会太高,如果能做到一周执行一次已经相当高了,压力测试给出的结论通常就是简单的通过or不通过,只有在不通过的时候,开发人员才会进一步通过log或者运用profile工具来进行,找出瓶颈并解决。

这里有个小小的问题是,当我们执行一次压力测试发现了部分问题后,开发人员直接修复了这些问题,等到下一次压力测试的时候,我们又可能发现类似的地方再次出现了性能瓶颈。也就是说压力测试不能做到如单元测试或者自动化功能测试那样频繁的反馈。

解决的办法也很简单,当我们分析出某段代码可能存在性能瓶颈之后,我们除了修复代码之外,还可以进一步针对这部分代码编写自动化的单元测试或者功能测试用例,这里需要一些技巧,当并不是很困难,这样做的好处是利用单元测试和功能测试的快速反馈的效果,来对压力测试已经发现的问题进行防护。

遗憾的是就是这么简单的一步,很多团队又止步于此了。

小结

敏捷软件测试的四象限分类方式已经广为人知,但遗憾的是大多数团队缺乏对其深入理解缺的能力,也缺乏主动提升能力的欲望,一旦碰到超过我们现有能力范围之外的时候我们往往就发挥了中国人的“聪明”,转而去采取某种简单or变通的方式,而且说服自己和周围的人相信这是某种提升,然而聪明反被聪明误,可惜……

你可能感兴趣的:(测试篇3——敏捷开发模式下的测试又是怎么失败的?)