和各种诡异 Bug 打交道 13 年,我总结了 18 个经验
最近我重新浏览了这所有的 194 个条目(历时 13 年),看看我从这些 bug 中学到了学到了那些重要的经验教训。我分为编码、测试和调试三大类。
在《程序员,你会从 Bug 中学习么?》一文中,我写了我是怎样追踪这些年遇到的最有趣 bug 的。
最近我重新浏览了这所有的 194 个条目(历时 13 年),看看我从这些 bug 中学到了学到了那些重要的经验教训。
我分为编码、测试和调试三大类。
编码
这些都是过去给我带来棘手 bug 的问题:
1.事件顺序
当处理事件时,问以下问题富有成效:事件是否可以以不同的顺序到达?如果没收到这些事件怎么办?如果事件在同一行出现两次怎么办?即使这通常不会发生,在系统的其他部分(或交互系统)中的bug也会导致它发生。
2.处理太早
这是上述“事件顺序”中的一个特殊情况,但是它已导致了一些棘手的bug,所以它自成一派。例如,如果信令信息接收得过早,在配置和启动程序完成之 前接收,许多奇怪的行为就会发生。另一个例子,当一个连接在被放入空闲列表之前就被标记为断开。当我们处理这个问题时,我们通常假设它处在空闲列表状态时 被标记为断开(但是当时它为什么没有从这个列表上撤下?) 没考虑到事情有时发生过早是由于我们没有想到。
3.隐蔽故障
例如,一些最难找的的 bug 是由于出现了隐蔽故障而继续执行而不是给出错误的代码导致的。例如,系统调用(如绑定)返回未检查的错误代码。另一个例子:当遇到一个错误元素时,直接返 回而不是给出错误的解析代码。调用在故障的状态下持续了一段时间,使得调试的难度加大。一旦故障被检测出,最好要及时返回这个错误。
4.If语句
含有多个条件的If语句(if (a or b),尤其是当嵌套时,if (x) else if (y)),给我导致了许多 bug。即使If语句在概念上很简单,当它有多个条件需要追踪时,很容易出错。最近我尝试重新把代码写得简洁,避免出现复杂的If语句。
5.Else
有一些bug的产生是由于没有恰当地考虑如果条件为假,什么应该发生。在几乎所有的情况下,每个If语句都应该有个else部分。而且,如果你在 If语句的一个分支中设置了一个变量,你也许应该在其他分支也设置该变量。与此相关的是标志(flag)被设定的情况。仅仅添加设定标志的条件很容易,但 是容易忘了添加应该重新设定标志的条件。任由永久性设定的标志留在那里可能会在将来导致 bug。
6.改变假设
一开始最难预防的许多bug是由不断变化的假设引起的。例如,最初仅仅只有一个客户,在这个假设下写了很多代码。后来某个时候,设计发生了变化,允 许每天有多个客户事件。当这种情况发生,就很难改变受到新设计影响的所有情况。很容易找到显式依赖该变化的所有项,但是难的部分是,找到隐式依赖旧设计的 所有情况。例如,可能有代码读取给定某一天的所有客户事件。一个隐式的假设可能是,结果集中元素的数量绝对不会大于客户数量。我没有好的方法可以预防这类 问题,欢迎读者建议。
7.日志记录
深入了解程序所做的任务是至关重要的,尤其是当逻辑复杂的时候。确保添加足够的(但也别太多)日志记录。那样你就能弄清楚为什么程序在执行它执行的任务。让一切运转良好时,它无关紧要。但是只要问题发生(这不可避免),你会很庆幸你添加了合适的日志记录。
测试
作为一名开发者,除非进行了测试,否则我不会说完成一项功能。起码这意味着每一行新代码或更改后的代码至少执行了一次。此外,单元测试或功能测试也 很好,但不够。新功能还必须在类似产品的环境下进行测试和探究。唯有这样,我才可以说完成了一项功能。下面是 bug 在测试方面给予我的一些重要的经验教训:
8.零(zero)和空(null)
务必要以零和空(合适的情况下)来进行测试。对于字符串而言,这意味着既指长度为零的字符串,又指内容为空的字符串。另一个例子:在发送任何数据 (零字节)之前,测试 TCP 连接的断开。没有使用这些组合来测试是 bug 悄然出现的头号原因,我在测试时是原本可以发现这些 bug 的。
9.添加和删除
新功能常常需要能够为系统添加新配置,比如说用于电话号码翻译的新配置文件。我们会自然而然的添加一个配置文件,来验证功能是否正常。然而,我发现很容易忘了还要测试配置文件的删除。
10.错误处理
处理错误的代码常常很难测试。最好由自动测试来检查错误处理代码,但有时这不可能。这种情况下,我有时采用的一招就是,临时修改代码,让错误处理代 码运行。要做到这一点,最容易的方法就是反转if语句,比如说将if语句由 error_count > 0反转为 error_count == 0。另一个例子是误拼数据库列名,让所需的错误处理代码运行。
11.随机输入
另一种往往能够发现 bug 的测试方法是进行随机输入。例如,H.323 协议的 ASN.1 解码可处理二进制数据。通过发送有待解码的随机性字节,我们发现了解码器中的几个 bug。另一个例子是使用测试调用生成脚本,其中调用持续时间、回复延迟、第一方挂断等都是随机生成的内容。这些测试脚本暴露了无数 bug,尤其是接踵而至的事件引起的干扰。
12.检查什么不该发生
通常测试包括检查一些需要的行为发生。但是很容易忽略他的对立面——检查不该发生的事确实没发生。
13.自制工具
通常,我创建了自己的小工具来使测试更简易。例如,当我处理面向 VoIP 的 SIP 协议时,我写了一个小的脚本可以返回正标题和值。这个工具使得测试许多个别场景变得简单。另一个例子是可以调用 API 的命令行工具。从小的开始,逐渐添加一些需要的功能,我最终有许多有用的工具,写自己的小工具的优势是我得到我想要的功能。
在测试中要发现所有的bug几乎不可能。有一次,我在一种情况下,我对处理关联号码做了改变,包括两部分:路由地址前缀(总是相同),和从000到 999的动态分配号码。问题是,当查找相关性时,动态分配的数字的第一个数字在查找之前被错误地删除。所以,不是寻找637之类的号码,你寻找的是37, 而这个号码不在表中。这意味着,它一直寻找到100,所以前100个调用正常,而之余的所有900个调用失效。所以除非我在重新启动之前测试了100多 次,否则在测试时发现不了这个问题。
调试
14.讨论
在过去对我帮助最大的调试方法就是与同事讨论问题。我常常只要向同事描述问题,就足以认识到问题是什么。此外,即使同事不是很熟悉相应代码,常常也能給出好主意,表明哪里可能有问题。我在处理最棘手的 bug 时,与同事讨论特别有效。
15.密切注意
往往是当调试一个问题很长时间时,是因为我做了错误的假设。例如,我认为这个问题发生在一个特定的方法中,事实上,这个问题甚至根本不会出现在这个 方法中。或者抛出的异常并不是我认为的那个。或者我认为最新版的软件在运行,但它其实是较老的版本。因此,一定要验证细节,而不是假设。它使你容易看到你 所期望看到的问题,而不是实际发生的问题。
16.最近的一次改动
本该运行的程序停止了,它通常是由最后的一次变动导致。有一次,最近的一次变动仅仅是日志,但是日志中的一个错误导致了更大的问题。为了让诸如此类的回归更容易找到,有必要在不同的提交代码中实行不同的变更,并且要清楚说明变更。
17.相信用户
有时当一个用户反馈问题时,我的本能反应是:这不可能,他们一定搞错了。但是我已经意识到我不应该这样做。我也不想这样,但更多次,事实证明他们报 告的问题实际上发生了。所以这些天,我认真对待他们的反馈。当然,我仍然反复测试所有的一切被正确地设置了。但是我碰过好多情况下,之所以发生奇怪的问 题,是由于不同寻常的配置或意料之外的使用,而我的默认假设是他们是对的,程序是错的。
18.测试修复的效果
如果你已经修复了 bug,还需要再测试。首先运行修复前的代码,然后观察 bug。然后运用修复再次测试。现在 bug 的问题应该被消除了。继续这些步骤确保它确实是一个 bug,确保你的修复已经修复这个问题。简单但很必要。
其他心得
过去13年,我一直在记录我遇到的最棘手的bug,很多事情发生了改变。从小的嵌入式系统,到大的电信系统,网页系统都做过。我使用的语言包括 C++、Ruby、Java 和 Python,若干类的 bug 在我使用 C++ 的日子里就已经不再出现了。像堆栈溢出,内存损坏,字符串的问题以及某些形式的内存泄漏。
其他的问题,像回路错误和极端案例,我见的少得多,因为我单元测试了更多逻辑,但这并不意味着那里没有 bug。这篇文章总结的经验教训,帮助我在编码、测试和调试这三个阶段尽量减小破坏。如果你发现了其他的技巧或者有用的技巧来预防或者找到 bug,请在评论区留言。
本文文字及图片出自 伯乐在线
你也许感兴趣的: