1LSGO软件技术团队
贡献人:LSGO船长
如果喜欢这里的内容,你能够给我最大的帮助就是转发,告诉你的朋友,鼓励他们一起来学习。
If you like the content here, the greatest helpyou can give meis forwarding, so tell your friends and encourage them to learn together.
在《Learning From Your Bugs》一文中,我写了关于我是如何追踪我所遇到的一些最有趣的 bug。最近,我回顾了我所有的 194 个条目(从 13 岁开始),看看有什么经验教训是我可以学习的。下面是我总结的最重要的经验教训,包括编码,测试和调试三个方面。
编码
下面这些都是我经历过的会导致难点 bug 的问题:
1. 事件顺序。在处理事件时,提出下列问题会很有成效:事件可以以不同的顺序到达吗?如果我们没有接收到此事件会怎么样?如果此事件接连发生两次会怎么样?哪怕通常不会发生,但系统(或交互系统)其他部分的 bug 可能会导致事件发生呢。
2. 过早。这是第一点“事件顺序”的一个特例,但它确实会引起一些棘手的 bug,因此我把它单独拎出来说明。
例如,如果信令消息在配置和启动程序完成之前就被过早接收,那么可能就会有很多奇怪的行为发生。
另一个例子:连接在被放进空闲列表之前就被标记为 down。在调试这类问题时,我们总是假定在空闲列表中的时候连接被设置为 down(但当时为什么不把它放到列表外面呢?)。
这是我们思考的不足,没有考虑到有时候事情会过早发生。
3. 悄无声息的故障。一些最难跟踪的 bug 有部分是由那些静静失败并扩展而不是抛出错误的代码所导致的。例如,没有检查代码却返回错误的系统调用(如 bind)。
又如:解析代码在它遇到错误元素的时候只是返回而非抛出错误。在错误状态中持续了一段时间的调用,会使调试变得更难。最好一旦检测到故障就返回错误。
4. If。有若干条件的 if 语句,if (a 或 b) ,特别是当有链接的时候, if (x) else if (y),都给我引发了很多 bug。即使 if 语句在概念上很简单,但当有多个条件要跟踪的时候依然很容易出错。这些天,我尝试重写代码使之更简单,以避免处理复杂的 if 语句。
5. Else。有一些 bug 是因为没有正确考虑到如果条件为 false 时会发生什么而引起的。
几乎在所有的情况下,都应该有一个 else 部分来应对每一条 if 语句。此外,如果你在 if 语句的分支中设置变量,那么或许你在另一个分支中也要设置。与此种情况相关的是标记被设置的情况。
只添加用于设置的标记的条件不难,但是很容易忘了添加当标记应该再次重置时的条件。留下一个永远设置的标志可能会导致之后接连不断的 bug。
6. 改变假设。许多一开始最难预防的 bug 是因为改变了假设所造成的。例如,在开始时,可能每天只有一个客户事件。于是很多代码是在这样的假设下写下的。但是后来,设计改变了,允许每天有多个客户事件了。
发生这种情况时,很难改变新设计影响到的所有情况。找到关于改变的所有显式依赖关系不难,难的是要找到所有隐性依赖于旧的设计的情况。
例如,可能会有获取给定某一天所有客户事件的代码。其中的隐含假设是结果集永远不会超过客户的数量。关于这方面的问题我也没有很好的策略方法,如果各位有的话,还请不吝赐教。
7. 日志记录。可视化程序做什么至关重要,特别是当逻辑很复杂的时候。确保补充足够多的(但不要太多)日志记录,这样你就可以说明为什么程序要这么做。
如果一切正常,那也没关系,但要是有问题发生,你会很庆幸自己添加了这些日志。
测试
作为一个开发人员,直到要测试了我才会去处理功能。至少,这意味着每一行新的或改变了的代码行至少已经被执行过一次。
此外,单元测试和功能测试都很不错,但还不够。新的功能也必须进行测试,并在类似于产品的环境中探索。只有这样,我才能说我完成了一个功能。下面是我经历过的 bug 所教会我的关于测试的一些重要的经验教训:
8. 零和 null。如果可行的话,确保总是用零和 null 来测试。对于字符串,这意味着要测试长度为零的字符串以及字符串为 null 两种情况。
又如:测试 TCP 连接的断开,要在发送数据给它发送之前。不使用这些组合方法测试是导致 bug 出现的首位原因。
9. 添加和删除。通常,新的功能包括能够添加新的配置到系统中——例如,一个用于手机号码转换的新的配置文件。测试它能否添加新的配置文件是很自然的。但是,我发现我们很容易忘记去测试删除配置文件是不是同样 ok。
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,并且此次修复的确可以解决这个问题。简单而有必要。
其他观察结果
在这 13 年来我一直在跟踪我所遇到的最棘手的 bug,很多事情由此而改变。我工作过小的嵌入式系统,大的电信系统以及基于 web 的系统。我使用过 C ++,Ruby,Java 和 Python。在工作于 C++ 时所遇到的几类 bug 已经完全消失,像堆栈溢出,内存损坏,字符串问题和某种形式的内存泄漏。
其他问题,如循环错误和边界情况,我看到的要少得多。但是,这并不意味着那里没有 bug。这篇文章中的经验教训旨在帮助减少编码,测试和调试三个阶段的 bug。如果大家有什么有用的预防和发现 bug 的技术方法,欢迎不吝指导。
作者:互联网
经过6年多的发展,LSGO软件技术团队在地理信息系统、数据统计分析、计算机视觉领域积累了丰富的研发经验,也建立了人才培养的完备体系。
本团队希望能与其他科研团队进行交流合作,并共同成长进步。
本微信公众平台长期系统化提供有关机器学习、软件研发、教育及学习方法、数学建模的知识,并将以上知识转化为实践。拒绝知识碎片化、耐心打磨技能、解决实际问题是我们的宗旨和追求。
领取专属 10元无门槛券
私享最新 技术干货