如何重现难以重现的bug

     生活中有这么一种现象:如果你关注某些东西,它就会经常出现在你眼前,例如一个不出名的歌手的名字,一种动物的卡通形象,某个非常专业的术语,等等等等。这种现象也叫做“孕妇效应”。还有类似的一种效应叫做“视网膜效应”,它讲的是:你有什么东西或者特质你就特别容易在别处发现你有的这类东西和特质。干了多年测试的我就会经常发现日常使用的系统中有很多的bug,而我老婆就发现不了。今天要说的事儿是“重现难以重现的bug”,这件事儿在本周共遇见了4次:第一次是微博上有一篇《程序员,你调试过的最难的 Bug 是?》(后面会附上);第二次是一个同事跟我抱怨,好几个bug难以重现特心烦,并问我怎么处理比较好;第三次是本周上线的产品出现了一个当时难以重现的bug,我们对它做了初步的分析;第四次是翻看史亮写的书《软件测试实战》,偶尔翻了翻,竟然看到一小节在写“处理难以处理的缺陷”。这时候,脑子里很多东西汇集到了一起,我想还是记录一下吧。下面是正文:

  也许测试人员(尤其是对新手来说)在工作过程中最不愿遇到的一件事情就是:在测试过程中发现了一个问题,觉得是bug,再试的时候又正常了。碰到这样的事情,职业素养和测试人员长期养成的死磕的习性会让她们觉得不能放过这个bug,但是重现这样的bug有时候需要花费大量的时间,有的时候还有一些盲目性(因为黑盒测试的缘故,很多内部状态是不可见的,因此无法获取有效的信息来做跟踪),效率较为低下。在实际工作中,时间和进度摆在那里,在经历了多次痛苦的失败尝试之后,测试人员的处理方法一般会有如下几种:1.向开发人员寻求帮助来重现bug;2.当做一个issue报给开发人员。可是这样的做法存在如下问题:

1.开发人员责任心不够强,不愿意花太多精力去求证这件事情,常见的回复就是:在我这儿没事儿啊,我也重现不了,bug关了吧。结果随后在生产系统上,bug又开始sui随机出现了。

2.就跟测试人员不擅长编码和调试一样,开发人员并不擅长找出bug。经过一番尝试以后,他们也找不出什么问题来,常见的回复同第一条是一样的。bug上线后又出现的宿命也是一样的。

      这时候,真正的问题来了:如何捕捉难以重现的bug?这件事儿对于测试人员来说就这么难么?

      答案并不那么乐观,重现“难以”重现的bug本来就是一件“难以”完成的事情。但“难以”并不是不可能,通过一系列的测试、分析方法,我们是能够抽丝剥茧把绝大部分隐藏的很深的bug揪出来的,当然有的时候你要考虑投入产出比,但投入产出比不是本篇要考虑的,本篇只讲一些我积累的经验。

      为什么不能重现bug?

      最大的原因就是:测试人员对被测物的了解还不够深入。

      我曾经做过一段很长时间的收集和统计,那些被称作过“难以重现”的bug最后都可以分为如下几类:

      1.环境的变更造成了bug难以重现,这里的环境包括了:基础软硬件环境(操作系统、网络、存储、中间件、容器等等),被测物自身发生了某些变更。环境的变更一般是由于多人共用环境造成的,也有少量情况下是系统内部或者时间触发的变更(这类bug非常难重现)。

      2.没有找到真正引发bug的操作。这些操作往往是一些不怎么显而易见的操作,测试人员在不经意间完成,而又忽略了这一操作,以致难于重现bug。

      3.没有找到真正会引发bug的操作序列。很多bug的重现需要满足多个条件。在满足多个条件的状态下,你做了对应的操作,bug才会被触发。

      4.bug必须使用特殊的数据才会出现,测试人员没有意识到她使用的数据的特殊性。一种比较难搞的情况是:同一组输入,在不同情况下(不是不同的业务情况)输入会被转化成不同的数据。我曾经见到过这么个例子,程序员用系统当前时间作为随机数的种子来生成id,但是id设置的比较短,一个存储的操作使用这个id,在一些情况下,发生了冲突,当时做功能测试这种小概率事件耗费了测试人员大量时间也没有稳定重现,还是在性能测试的阶段测试了出来。

  5.测试人员由于错误操作,出现了误报(这很常见)。比如,记得自己执行了step3,其实没有,或者没有正确执行step却觉得正确执行了。

     怎样对付这样的bug呢?

     我喜欢James Bach 说的那句话:测试就像CSI。CSI是Criminal Scene Investigation 的缩写,也是我非常喜欢的美国系列剧。

                                              

      从我来看CSI的精髓在于:仔细观察,详细记录,科学分析,严密推理,有序求证,大胆假设,持续不懈,团队协作和一点儿运气。找bug其实和CSI探员做犯罪现场调查没什么太大区别。得知道,你工作的重要性一点儿不亚于CSI探员。

      仔细观察:第一件事情就是要观察,观察你所做的一切操作和一切相关的系统反馈。在一开始,观察的重要性要远远大于思考,通过观察你获得蛛丝马迹,这些蛛丝马迹是你进行思考和假设的关键输入。例如,我在一次测试的过程中,发现做某种操作的时候会相当慢,极少数情况下还报错过一两次,当询问了开发人员后得知这个操作的后台实现步骤是:先查看数据是否在缓存中,如果不在,则从远端服务器请求数据。我抓住少数情况下会报错的这一现象,仔细观察它的出错信息后猜测报错并不是因为网络连接不稳定引起的,而是由于远端服务接口实现有问题引起的,后来重新设计了测试用例,果然找到了问题所在。如果不仔细观察出错信息,就会听信开发人员认为这是网络不稳定引发的正常issue而错过这个bug。

      详细记录:详细记录你的操作步骤及返回结果非常有助于回朔问题,也有助于后续分析。准备一个word文档,和截图工具有时候非常必要。另外,在观察的时候,你不仅要注意被测物的最终返回,还需要观察过程中的一些中间状态,往往这些中间状态提供的信息才是解开问题的关键。这些中间状态一般会被记录在log文件中,因此知道你的被测物是如何记log的,log被记录在哪里非常重要。log给了你另外一个看系统的角度。log是有级别的,如果级别可以动态调整,在找比较难找的bug时,可以将log记录的级别调至最低(DEBUG级)让它们记录更多内容。利用系统的错误转储文件(比如linux的core dump文件,windows下也有相应的记录转储文件的方式)分析也是个不错的办法(需要较高技术能力),但一般建议测试人员把这些转储文件交给更专业的开发人员来分析。在我短暂的C++开发岁月中,有使用过GDB阅读转储文件的经历,那绝对不是愉快的回忆。你瞧,测试人员的主要工作是找出可重现的bug,并不是定位它们,不是么?

    除了log,如果能有监控信息,也要查看他们。比如系统提供的监控平台,监控日志;应用自己的监控平台、监控日志(如果有的话);采用一些外部技术手段截取一些中间状态信息,如使用sniffer抓取通讯包,使用Fiddler截获HTTP报文内容;给运行程序插桩来查看内存,堆栈,线程,函数被调用情况等情况,如Jprofile,gpertool等等。

     科学分析:对于黑盒测试人员来说,科学分析意味着你需要有一定的分析策略。我们需要采取一些形式化的方法来完成我们的分析。基于我的统计,缺陷难以重现有很大一部分原因是因为“没有找到真正引发bug的操作序列“。测试人员不可能像开发人员那样去跟入到代码内部,设置断点调试程序,他们主要的测试方式是直接来操纵被测物,并从外部观察被测物状态的改变。显而易见,“状态转换图分析法”是测试人员对付“难以重现bug”的最强有力武器之一。状态转化图能够帮助测试人员很好的选择操作路径,并且知道这么做有什么意义。“状态图转化法”绝对是测试人员值得花时间学习和研究的一种方法,你可以走的很深,也可以研究得很远(可以从MBT的方向进行拓展),限于篇幅,这里就不展开了。在这里推荐《探索吧!深入理解探索式软件测试》这本书,它的第八章对“状态转换”做了非常实用的描述。

     上文分析的让bug难于重现的另一种原因是没有找到“真正引发bug的特殊数据”。我的常用做法是这样的:1.画出系统交互图(要真正弄清系统的边界,这很重要),并识别出每种交互会有什么样的输入、输出数据和中间数据,识别出这些数据的规约和格式,这样你就不会对数据有遗漏。2.考虑数据的等价类、边界值,对这些输入进行组合,分析数据之间是否有耦合关系,如果有耦合关系,弄明白关系是什么,设计违背这些关系的用例,最后采用组合测试工具初步生成测试集,再人工选取最可能出问题的数据集(直觉有时候非常管用)。

     严密推理:天马行空对测试人员很重要,但是当你试图重现一个bug的时候,这并不是一个非常好的方法。抓住了蛛丝马迹,你就要推理是为什么产生了这种蛛丝马迹。限于工作性质,测试人员更多的会从:业务完整性、数据完整性、业务正确性、数据正确性等方面考虑问题。但是,如果测试人员对被测物的IT架构有比较深入了解的话,推理的范围会扩大到技术实现层面,如:多线程可能引发的问题,网络引发的问题,excepiton处理不当引发的问题,全局事务设计不当引发的问题,内存泄漏引发的问题,数据库表设计不合规引发的问题等等等等,这些会让你的分析推理能力如虎添翼。当然,如果限于条件,测试人员不具备这类能力,则应该在适当的时候请求开发人员协助。

     有序求证:这里只有一点需要注意。那就是,在求证的过程中不要打散弹枪,按照你的推理一步一步的来,一个个推理的来验证,一次只引入一处修改。这样才能让你的捕虫网编制的足够细密。

     大胆假设:有的时候,看似八竿子打不着的东西竟然存在着千丝万缕的联系,而你获取信息的过程总是一个由少及多的过程,一开始这些联系是无法被识别出来的。通过推理,逐步验证,你慢慢的识别出了大部分内在联系。但有时候这种逐步推进的工作也会有局限性,工作如果出现了瓶颈(你试遍了你所有的假设,都没有重现bug),这时候就需要发挥一点儿想象力了,天马行空这时候从一定程度上又变得有用起来,当然天马行空也不是无厘头,还得靠我们所谓的“灵光一闪”,这号称是潜意识在帮助你。CSI的剧情中不也总是出现这种柳暗花明的桥段么?

     坚持不懈:话不多说,有的时候你差的就是那么一点儿点儿耐心。

     团队协作:很多情况下,重现bug不是一个人能搞定的。我们需要测试环境ready,测试数据ready,各种监控、分析工具ready,各种不同的视角开拓思路、加深对被测试物的认识。这是team work!!!独行侠有时候很管用,但是终究有极限。这就是为什么CSI是一票人在做而不是一两个人在做。

     一点儿运气:说实在的,有的时候重现bug就是靠运气,你不得不承认这一点。事实上很多美好的事情发生都得依靠运气,比如中彩票。要记住的一点是,运气是建立在你不懈努力的基础上的。如果你一张彩票不买,你肯定什么也中不了。但如果你坚持买上几年,中个五块十块甚至二百也不是梦。

      Let it go:全试过了,连运气都没有。你只能放手,回到最上面我说的那两条了:找开发来帮忙,或者给开发报issue。btw,即使不能重现bug,也应该给开发人员提供更多信息:如log、dump文件、监控记录、屏幕截图等。做一个负责人的测试人员,把烦恼真实的留给下家,这很重要:)

 

最后给出一个软件调试大牛 David.A.Agans对于软件调试的九条建议,非常值得一读:

http://sydney.edu.au/engineering/aeromech/MTRX2700/Course%20Material/lectures/PDF/week04/Debugging.pdf

 

9月25日:今天学到了一个词:Heisenbug ,这词的中文意思可以被翻译为“神出鬼没的bug”。。。这个单词和量子力学元勋海森堡的名字差了一个字(Heisenburg) 量子力学的一个经典定理就是"测不准原理"。 大家的吐槽能力真强。 想了解细节请看下面的这篇文章:

http://blog.csdn.net/kjfcpua/article/details/8125306

 

9月26日:觉得有一些实例会更容易理解,我会尽量收集一些例子放到这个帖子里: 

记一次w3wp占用CPU过高的解决过程(Dictionary和线程安全)

12月3日更新: 一个Ajax同步异步引发的血案  一个hotfix的debug过程:)

 

12月3日:其实上面的大段论述是站在测试人员角度上来看的。我也写很多代码,作为一个开发人员(哈哈)我查错的方式一般是:

1.先能稳定的复现错误。(这一步最难)

2.设桩,打印出一些中间状态来分析,看到那一块儿错了。

3.摘除不相干的代码,慢慢迭代,定位错误。

4.如果发现调用第三方依赖跟你想的不一样,先读文档,然后再测试第三方依赖。有问题再想办法。

5.良好的程序设计和单元测试覆盖度才是不二法门,会让你的调试变得简化的太多。。。用过都说好:)

6.测试工作的确增强了我排错的能力。coding的同学都尝试做几个月测试吧。会有相当大的好处。

 

14年12月15日:早上的一个同事提出了一个小技巧,觉得很不错,记录一下:如果你没有办法稳定的复现bug,可以通过概率统计的方式将问题反馈。如果10次里出现一次问题,不足以打动开发人员重视问题,可以通过自动化测试的方式提高执行次数的数量级,如果一千次出现50次~100次。这个问题就足够引起重视了。在negotiate的时候,这是一种好思路。

 

 

----------分割线,下面是转载的文章--------------------------------

posted @ 2014-09-23 00:22  skytraveler  阅读(...)  评论(...编辑  收藏