测试主要是发现错误,调试(也称纠错)则是确定错误的原因和准确位置,并加以纠正。
调试是包含2个步骤,从执行了一个成功的测试用例、发现了一个问题之后开始。第1步,确定程序中可疑错误的准确性质和位置;第2步,修改错误。错误定位是一项技术活,是有一定难度的。目前有4种常见方法。
1. 蛮干(brute force)(也叫暴力法)
调试程序的最为普遍的模式是所谓的“暴力”方法。这种方法之所以流行,主要有2个原因:
1)它不需要过多的思考,是耗费脑力最少的方法。新手上路,都喜欢这种方法。
2)人遇到bug,可能会自尊心受损、热情耗尽、也可能迷失方向,(总之心情不好,需要发泄啊),所以使用暴力虐待程序。
暴力调试方法,主要有3种类型:
(1)利用内存信息输出来调试
使计算机将存储器的全部内容和地址打印出来,然后在这数以万计的数据中寻找错误所在。这种方法也叫主存信息转储,虽然有时能获得成功,但更多的是白白浪费时间、纸张和人力。
(2)在程序中各点设置打印语句
本人,大学时代常用这个方法,一个print,输出变量值。。。
(3)使用自动化的调试工具进行调试
自动化调试工具的工作机制,类似于在程序中插入打印语句,但是不修改代码。
还有一个效率改进型的暴力法。
(4)2分查找(对分查找法)
对分查找法。这种方法主要用来缩小错误的范围。如果已经知道程序中的变量在若干位置的正确取值,可以在这些位置上给这些变量以正确值,运行程序观察输出结果,如果没有发现问题,则说明从赋予变量一个正确值开始到输出结果之间的程序没有出错,问题可能在除此之外的程序中,否则错误就在所考察的这部分程序中。对含有错误的程序段再使用这种方法,直到把故障范围缩小到比较容易诊断为止。
个人经验,暴力调试方法有不少缺点:无关数据量很大;有时需要修改代码(可能忘记删除调试代码);效率底下……
这些暴力调试方法的主要问题在于:它们都忽略了思考的过程。
我们可以在调试程序和侦破谋杀案之间找出相似点来。实际上,在几乎所有的谋杀悬念小说中,谜案都是通过仔细分析线索,将表面上不重要的细节全联结起来而最终侦破的。这不是一个使用蛮力的方法,要使用蛮力的是寻觅障碍物或搜寻财宝。
还有一些证据表明,无论调试小组成员是富有经验的程序员还是学生,肯动脑筋而不是依赖别人帮助的人能够更快、更准确地发现程序的错误。因此,我们建议仅在下列情况下使用暴力调试方法:
(1)其他的方法都失败了(可以结合“2分查找”来逐步缩小出错范围)
(2)作为思考过程的补充(辅助方法),而不是替代方法
2. 消除原因(cause eliminatio)
通过思考列出可能的原因,逐个排除,最后找出问题所在。这种方法又分为归纳法和演绎法。
2.1归纳法(induction)
归纳的思考过程就是从特殊到一般,从线索(即错误的症状,可能是一个或多个测试用例的结果)出发,寻找线索之间的联系、概括出共同特点、得出一般规律。归纳的过程如下图所示。
它分为如下4个主要步骤。
(1)确定相关的数据
调试人员犯的一个主要错误是未能将所有可用的数据或症状都考虑进去。第一步是列举出所有知道的程序执行正确和不正确之处,这些不正确之处即是症状,让我们相信确实存在错误。那些相似而不相同、且未引起症状出现的测试用例提供了额外的有价值的线索。
(2)组织相关数据
归纳意味着从特殊到一般,因此第2步是组织这些相关数据,以便观察线索间的模式。
最重要的是搜寻矛盾。
(3)作出假设(注1)
研究线索之间的联系,利用线索的结构中可能的模式作出一个或多个关于错误原因的假设。如果无法作出假设,就需要更多的数据,这些数据可以通过设置和执行附加的测试用例来得到。如果有多个假设存在,首先选择最有可能的一个。
注1)所谓假设,就是对于怀疑所产生的问题,提出一种推测性的或可能性的说明。
(4)证明假设
此时,常犯的主要错误是跳过这一步而进入结论阶段,并企图确定错误所在,这是纠错中经常发生的问题。但是在修改错误之前,证明这些假设的合理性是极其重要的,忽视这一点将导致只能确定问题的一个征兆或一部分征兆。假设的证明是靠比较它与最初的线索或数据,确信这些假设可以完全解释这些线索的存在。如果无法解释,要么这些假设是无效的或不完整的,要么还有更多的错误存在。
2.2演绎法(deduction)
演绎的过程是从一些普遍的理论或前提出发,使用排除和精炼的过程,达到一个结论(错误的位置)。过程如下图所示。
举个谋杀犯的例子,与归纳过程相反,首先从一系列嫌疑人入手,通过排除(花匠有当时不在现场的合理证词)和提炼(罪犯可能是红色头发)的过程,判断出管家可能犯了罪。演绎的步骤如下:
(1)列举出所有可能的原因或假设
第1步是建立1份所有想象得到的错误线索的清单,线索不需要有完整的解释;它们纯粹是一些推测,帮助我们组织和分析现有的数据。
(2)利用数据排除可能的原因
详细检查所有的数据,尤其寻找存在矛盾的地方,然后尽量排除所有可能的原因,仅留下1条。如果所有的原因都排除掉了,需要增加额外的测试用例,得到更多的数据来设计新的推测。如果剩下的原因多余1个,那么首先选择最有可能的原因,即主要假设。
(3)提炼剩下的假设
此时的可能原因也许是正确的,但可能不够具体,不能指出错误来。因此,需要使用现有的线索来提炼这个准则。举例来说,我们可能首先想到“对文件中最后一行的处理可能存在错误”,将其更加具体化,提炼为“对文件最后一行的文件结束指示器的处理,可能存在错误”。
(4)证明剩下的假设
这个重要步骤与归纳法中的第4个步骤相同。
3. 回溯法调试
在小程序中定位错误的一种有效方法是沿着程序的逻辑结构回溯不正确的结果,直到找到程序逻辑出错的位置。也即,从程序产生不正确结果的地方开始,从该处观察到的结果推断出程序变量应该是些什么值。在头脑中,从这个位置开始逆向执行程序,重复使用“如果程序在此处的状态时这样的,那么程序在上面位置的状态就必然是那样的”过程,就能很快定位出错误。
适用对象:小程序;代码非常独立的模块等。
4. 测试法调试
最后一个“思维型”的调试方法是使用测试用例。考虑下面两种类型的测试用例:供测试的测试用例,其目的是暴露出以前尚未发现的错误;供调试的测试用例,其目的是提供有用的信息,供定位某个被怀疑的错误之用。换句话说,当发现了某个被怀疑的错误的症状之后,我们需要编写与原先有所变化的测试用例,尽量确定错误的位置。
实际上,这种方法不是一个完全独立的方法;它常常结合归纳法一起使用,以获得进行假设和/或证明假设所需的信息。它可以和演绎法一起使用,以排除嫌疑的原因,提炼剩下的假设,并/或证明假设。
5. 建议
如果,曾经遇到过类似错误,或者有过类似经验,建议使用演绎法。
↓↓
如果错误从来没有遇到过,请使用归纳法。
↓↓
下列场合,建议使用回溯法
1)程序比较小
2)模块比较独立
3)已初步知道出错代码位置(通过查看log等,已初步知道代码范围)
↓↓
如果,归纳法、演绎法、回溯法都无法奏效,请使用暴力法(死马当活马医)。此时,请结合对分查找法,这样可以提高效率0.5~1倍。
附录
为方便打印和查看,做一个debugging 整体图。
转载于:https://my.oschina.net/u/658505/blog/751635