测试
指通过运行程序以确定它是否按照预期工作。
调试
则指修复已知的未按预期工作的程序。
测试和调试的关键
就是将程序分解成独立的部件,可以在不受其他部件影响的情况下实现、测试和调试。
关于测试,最重要的是清楚它的目的是证明错误的存在,而不是证明程序没有错误。
测试的关键就是找到极有可能产生错误答案的一组输入,可以称之为测试套件
找到测试套件的关键
是,对所有可能的输入空间进行分区,将其划分为对程序正确性提供相同信息的多个子集,然后构建测试套件,使其包含来自每个分区的至少一个输入。
如果使用来自每个子集的至少一个值对函数实现进行测试,就非常有可能暴露可能存在的错误。
基于代码探索路径的启发式方法称为白盒测试
。
基于规范探索路径的启发式方法称为黑盒测试
。
如果一个白盒测试套件可以测试程序中所有潜在路径,那我们就可以认为它是路径完备
的。一般来说,路径完备不可能达成,因为这取决于程序中循环的次数和递归的深度。
白盒测试提供的一些经验准则
:
测试所有if语句的所有分支。
必须测试每个except子句。
对于每个for循环,需要以下测试用例:
对于每个while循环:
对于递归函数,测试用例应该包括函数没有递归调用就返回、只执行一次递归调用和执
行多次递归调用的情况。
测试一般分为两个阶段
。第一个阶段称为单元测试,第二个阶段称为集成测试。
第一个阶段称为单元测试
。在这个阶段中,测试者构建并执行测试,
用来确定代码的每个独立单元(例如,函数)是否正常工作
第二个阶段称为集成测试
,用来确
定整个程序能否按预期运行。
在工业界,测试过程通常是高度自动化的。测试者不会坐在终端前面手动输入用例并检查输出。他们会使用测试驱动程序
显性错误
有明显的表现,如程序崩溃或运行时间异常长(可能永不停止)
隐性错误
没有明显的表现,程序会正常结束,不出任何问题——除了给出一个错误答案
持续性错误
在程序每次使用相同的输入运行时都会发生
间歇性错误
仅在某些时候出现,即使程序使用相同输入并在相同条件下运行
优秀的程序员编写程序时,会尽量使程序错误是显性的和持续性的,这种编程方式通常称为防御性编程
多数程序员认为最重要的调试工具是print语句
如果将调试看作一个搜索过程,那么每次实验就要尽力缩减搜索空间。
缩减搜索空间
的一种方法是,设计一个实验,确定代码的一个具体区域是否是造成某个问题的原因。另外一种缩减搜索空间的方法是,减少导致错误出现所需的测试数据量。
系统地缩减搜索空间,最好的方法是执行二分查找
。先找出代码中间点,然后设计一个实验,确定是否因为中间点前面存在问题才导致程序出现这种症状
调试遇到困难
时,我们该怎么做呢?
排除常见错误。例如,看看你是否犯了以下错误:
不要问自己为什么程序没有按照你的想法去做,而要问自己程序为什么像现在这样做。后者应该更容易回答,要想弄清楚如何修复程序,这可能是一个很好的开始。
记住,错误可能不在你认为会出错的地方。如果在那里,你早就应该发现它了。确定错误位置的一种实用方法是,看看那些你认为不会出错的地方。
试着向其他人解释程序的问题。每个人都会有盲点。经常有这样的情况,试图向别人解释问题的时候,你会突然发现自己忽略的地方。向其他人解释为什么程序中某个地方不会出现错误是个很好的选择。
不要盲目相信任何书面上的东西。特别是,不要相信文档。代码行为可能与注释不一样。
暂停调试,开始编写文档。这会帮助你从不同视角接近问题所在。
出去散散步,明天接着做。这可能意味着与你坚持工作相比,修复问题的时间要晚一些,但花费的总时间会大大减少。也就是说,我们使用时间上的一点延迟换取了效率上的大幅提升。(同学们,开始习题集中的编程练习吧,宁早勿晚,这是个绝好的理由!)
我们的目标
不是修复一个错误,而是快速有效地得到一个没有错误的程序。你应该扪心自问,这个错误能够解释所有观测到的症状,还是只是冰山一角。如果是后者,最好将对这个错误的处理与其他修改结合考虑。