调试 Web 应用的准则

518 查看

无论编码能力如何,我们都会犯错。不仅自己代码中会有 bug,多数的 web 应用是由团队创建的,所以队友和你一样,都有可能犯错。此外,接手遗留代码意味着同时继承了其它团队的 bug。在此情况下,调试技巧就派上用场了。

花点时间和精力去提升调试能力是值得的。我会介绍一些小窍门来拓展你的调试技巧,也会给出一些建议,以便更容易的定位 bug。

本文主要是关于调试原则的,不会深入具体的技术细节,也不会探索有关浏览器和开发工具的技巧。下篇文章会更加专业,介绍实际操作中应用的调试工具。

了解成果

最重要的调试工具就是你的想法。我可以告诉你很多调试技巧,但是只有在你拥有足够的特定知识,可以在上下文使用它们的情况下才会对你有所帮助。在你知道问题是什么之前,不要尝试使用这些技巧。

调试工具的目的是验证假设。最重要的是首先要知道代码应该做什么。

如果你发现自己已经在代码的不同位置插入了10个断点,那么你就错了。如果你知道问题在哪的话,你就应该知道在哪里插入断点。消息记录也是同样的。如果有10条不同的消息,你也无法确认你在找什么。做做功课,试着用不执行代码就能理解它。

执行环境很有帮助

编写可测试的代码。假设你经常给代码做 单元测试,那么你应该可以创建业务对象并且在执行环境中调用它们的方法。最简单的执行环境是你选择的开发工具。你可以使用控制台创建对象,调用它们的方法。在 Mocha, Chai 和 SinonJs 可用的调试模式下,如果应用可以使用的话也很有帮助。

开发团队的基本训练应该包含练习如何使用应用的对象。

执行环境可以有多种用途:

  • -通过观察特定输入所对应的输出结果,验证代码的构思模型
  • -对方法进行黑盒测试来验证你所推断的fault
  • -重现failure场景

改正原因,而不是表象

在前面的部分,我特意使用了 fault  和 failure 这两个单词。如果你不知道 fault、error 和 failure 之间的区别,可以读读 此文 的“Fault,error,failure”部分。

人类健康和修补 bug 很相似。好比一个名叫 John 的病人因缺少维他命而感冒了。医生告诉他服用扑热息痛来缓解症状。扑热息痛隐藏了很多的症状,直到 John 好起来。感冒好了以后,John 仍旧不会想着去改变自己的饮食。

这中情况在代码上就体现为设计糟糕的意大利面条代码(指那些结构上完全不可理解的系统)。这里的“病症”就是没有完全理解代码的开发者写出来的 fault。假设这个 bug 造成了 10000 € 的损失,公司会给开发团队施压以即刻修复 bug。开发者们快速的了解 bug 的表现然后用“扑热息痛”来对付:

和发高烧一样,在紧急的情况下,病症需要即刻处理。这种修补方式看起来起作用了,所以开发者就将它用到产品中。多数情况,造成 bug 的原因却被忽略了。这会造成什么问题?

从长期的角度看,扑热息痛会破坏人体的肝细胞。只解决表象问题的代码会摧毁软件的可维护性。我们应该抛弃快速修补的想法,而去正确修补造成 bug 的真正原因。

如果 bug 不紧急的话,一定要查找 bug 原因。确保你完全理解了 failure、error 扩展和实际的 fault,并且修补 fault 要遵守业务对象的作用和职责。例如,在依据 MVP 模式编写的代码中,千万不要在 presenter 中修改 model 的错误。

假设代码很难调试或修改,那么可以考虑重构代码。这就涉及下一部分的内容了。

调试困难的代码需要重构

当开发者觉得测试、调试、更改代码困难时,就要考虑重构了。重构的好处在于保持功能相同,又可以提升代码的可维护性。

设计糟糕的代码有以下特点:

  • WET 代码(We Enjoy Typing)
  • 不清晰的作用和职责
  • 冗长的方法、高循环复杂度
  • 糟糕的或不清楚的命名
  • 相同类多个方法中有重复的 switch 语句,涉及到该类的不同子类

如果代码难以理解,那么最终一定会产生 bug。即使是利益相关者都应该知道新特性和可维护性之间比例合理的必要性。修复你了解的 fault 总比调试可读性糟糕的 failure 成本要低。

一次测试一条假设

回想一下你不得不对代码做出假设,来看看哪里出错了。一点控制台日志和断点就可以测试你的假设。或者你可以在报错的上下文中插入一个断点,然后用开发工具的控制台创建一个执行环境。

当假设证明是错误的,就要在验证下条假设前清理断点和控制台日志。如果你之后想要回顾这些日志和断点,可以用 Git 贮存。

日志和断点的注释没有意义,因为你不知道哪个调试声明是属于哪个假设的。

一旦调试器声明和日志堆在代码中,它们通常会干扰你。这种干扰会使你从定位 bug 的清晰思维中分散注意力。

测试驱动的 bug 修复

一种再现 bug 的方法是基于 bug 描述编写失败的测试用例。即使测试通过了,你也不是在做无用功:这个测试也减少了应用的 bug。最后你总会找到试错用例,捕捉到 failure。

回溯错误扩展,定位错误。当你理解了错误的原因后,用试错用例进行测试。这些测试会指引你修补 fault。测试的最大好处是,只要坚持在每次部署前都执行单元测试,同样的 bug 就再也不会在产品中出现。

不要在产品上调试

我曾听说不愉快的事,发生在由于一些特殊数据只能在实际的数据库中找到,而在产品上调试代码时。这是很外行的行为。

像这种情况下,可以参考以下方法应对。

如果开发者有实际数据库的权限,复制数据库是一种选择。在一些情况下,这会花费很长的时间。解决方法是可以用专门的转储脚本只复制相关数据到开发者的数据库。

也有可能开发者对一些敏感数据没有权限。这种情况下糟糕的惯例是每当 bug 严重损害公司利益时,开发者便会收到一份临时报告。任何解决方式都比这种好。可以是创建同类匿名数据的转储脚本,或者是一系列精确的 bug 报告,给开发者足够的数据和追加问题的机会。你的开发团队应该时刻有应对紧急情况的计划。

忽略权限的问题,要确保开发环境的数据库和产品中同量级的最新数据保持定时更新。这不仅有助于再现 bug,对定位性能瓶颈也有帮助。

作为一名前端开发者,你可能会遇到只在特定设备或浏览器下出现的 bug。或者 ad blocker 软件会阻碍你的 AJAX 请求。尽可能多的收集环境信息,同时确保可以和遇到问题的用户进行沟通。

要捕捉只在特定环境下出现的 bug,更主动的方法是使用一款错误追踪应用,比如 SherlogJs。错误会在用户考虑给你发送 bug 报告之前出现在你的仪表盘上。有许多 bug 从不会上报,所以这是捕捉这些 failure 的唯一方法。

Bug 无法在开发环境再现还有另外一个原因。额外的库和附加服务可能会迫使你在产品上收集信息。修补这些 bug 需要专门的支持以及与其它团队间的良好合作。建立不依赖产品环境就可测试的服务总是很重要。

总结

基于 bug 描述再现 bug 后,思考一下哪个地方错了。构想出假设并且研究代码。你应该清楚的知道哪个类和哪个方法可能会受到影响。应用相关的业务对象来验证你关于代码和 bug 的假设。当使用对象或调试时,一次性只核实一条假设来排除干扰项。修改 fault,而不是 failure。如果调试或修复很苦难,可以考虑重构代码。确保你修补后的代码全面的测试过。在 failure 很难重现的情况下,考虑升级基础设施以支持局部调试。

此文就要结束了,我没有阐述调试器声明如何生效,或者应该如何使用浏览器的开发者工具,希望你没有失望。调试不仅仅是工具。希望你可以收获一些有用的观点,更加善于捕捉 bug。如果你对调试工具感兴趣,请看下篇文章