解决"Bug"的正确姿势

方法论

发现问题

“嘿,我觉得哪里不太对,是不是有bug?”

你的期望是什么?

在抛问题给别人之前,应该多想一下,“这是个bug吗?你对它的期望是什么?”。如果你的期望与实际行为相符合,那就不能算是个bug。如果跟你的期望不符,也有可能是以下原因:

  • 使用方法不对(比如,错过了必要的步骤)
  • 你的期望可能不准确
  • 在旧版本上面做测试
  • 在不受支持的环境中使用
  • 尚在开发中的新功能
  • etc.
  • 真的是bug

别动辄声称找到bug

所以就算与你的期望不符,也可能不是bug,所以在抛出问题之前要做好确认,这样可以节省大家的时间(你甚至能够在确认这是不是bug的过程中就能独自解决问题)。此外如果冒然抛出(不是bug的问题),对方可能会感觉被冒犯,而忽略你的问题。

“最小可重现例子(Minimal Reproducible Example)”

“那么,怎么样确认这是不是bug呢?”

我们所处的环境往往比较复杂,比如若干服务之间的调用依赖、复杂的库依赖、代码本身的复杂度等等。在复杂的环境比较难以确认是不是bug,就算确认了也比较难验证是不是真的解决了问题。

一个常见的方式就是最小可重现例子(Minimal Reproducible Example)。最小可重现例子可以拆分为三个部分:

  • 最小:往往是在某个环境下面的包含尽量少内容的一条命令、十几行代码、固定的若干操作步骤、一个test case
  • 完整:提供完整的信息,比如参数、配置等,复现问题不可或缺的信息
  • 稳定复现:可以很方便的直接运行就能够复现问题,而不是时有时无

那么,怎样构造出最小可重现例子呢?这里介绍三板斧:稳定复现、缩小范围、解决问题。有同学可能会问,“为啥这最后一步是‘解决问题’呀?”。其实在你缩小范围到最小的时候,往往很容易发现问题的原因,多数情况下都能够独自解决问题了~

稳定复现

稳定复现指的是,只要follow固定的步骤,就一定能够重现问题。然而在寻找最小可重现例子的初级阶段,往往是较多的步骤和较大的scope和依赖。剩下的就是靠缩小范围来减少步骤和scope了。

缩小范围

最小的范围往往是:只要减掉任何一点点都不能重现问题,拥有最少的依赖,最少的代码和配置。缩小范围目的是使得复现问题时的依赖最少、复现的代码最短、逻辑最简单。这样就可以比较容易的排除无关因素,也更容易找到问题的原因。

缩小范围的方式我认为有两种,适合两种不同的情况:

  1. “大胆假设,小心求证”。适用于对于代码比较熟悉,可以做逻辑推理的情况。 这种情况下往往可以比较快速的推想到出问题的大概位置,当然这里还是需要通过“稳定复现”来验证是不是当前的范围。“大胆假设”的原因是,如果你不能一眼就发现问题的原因,往往是有一些例外、异常的情况没有考虑到,这个时候常规的逻辑可能会忽略掉一些本该考虑的情况,这个时候就要有一点脑洞。

  2. “二分法”。适合对于代码不太熟悉,或者完全没有头绪的情况。

    • 例如,如果你的库锁定了在2.1.1和2.1.2的release之间出现了问题,而这两个release之间有20次提交,你可以通过二分法快速在20次提交中找到引入问题的提交(这里也提一下小批量多次提交的重要性,如果一次提交N个文件,那就会麻烦一点了)。
    • 再例如,如果你发现本来应该正常编译的scala代码编译失败了,想找一下原因,你可以:
      1. 确认确实编译失败了,而不是IDE的bug,首先通过sbt/gradle/maven命令行编译试试;
      2. 如果代码比较短,建议直接在REPL中尝试。如果项目较为复杂,可以开启“二分查找模式”了,缩减项目中的模块,如果能复现就继续缩减,直到不能复现为止;
      3. 上述步骤得到的代码可能仍旧比较长或者比较复杂,需要继续缩减。可以删除与问题无关的代码定义,如果能复现就继续删除,直到不能复现为止;
      4. 上述步骤得到的代码应该相对较短了,但是,如果你是使用gradle/maven/sbt这样的工具来编译,仍然有可能遇到的是工具的bug,而不是官方编译器的bug,所以需要通过javac/scalac/kotlinc这样的官方编译器来验证,判断究竟是哪边的问题。

tips:在得到最小化的例子之后可以将它保留为test case,这样可以防止以后出现类似的问题。

找到根源

得到最小化的例子之后,往往比较容易就能够判断出是哪里出了问题。也比较容易验证问题是否被解决了:如果最小化的例子不能够继续复现问题,那么问题就可以认为被解决了。

如果是scala代码,最常见的问题可能是编译器推断不出来类型导致编译挂掉了,需要你手动写一下类型作为workaround。如果是一个引用的外部库,往往可以通过升降版本来解决。

“如果我解决不了呢?”

寻求帮助

这时候就需要寻求帮助来解决问题了,寻求帮助可以分两步

  • 通过搜索引擎、阅读官方文档、查找issue、搜索StackOverflow等方式自行解决问题
  • 通过询问同事、提交issue、StackOverflow提问、邮件等方式解决问题

如何描述问题

这两步都有一个前提就是如何简短而准确的描述你的问题。一般情况下error message就是最准确的描述方式了,但是你肯定遇到过一些莫名其妙的error message,这时候就需要加入一些其他的关键词来更好的描述问题了。试想以下两种描述方式:

  • scala build failure
  • scala / GADT / type infer failure

后者显然更容易搜索到相关内容,这里我用斜线分割了以下,GADT是用到的功能,type infer failure是错误类型。第一个描述方式的”build failure“显然太过宽泛,既没有说明是跟什么特性相关的,也没有说明是什么类型的错误。

在寻求帮助前

在得到了准确的描述方式之后,大多数问题都可以通过搜索引擎、官方文档、issue等直接找到解决办法或者是workaround。

如何问问题

关于如何问问题这个问题,How To Ask Questions The Smart Way 是我所知道的最好的答案了。

总结

“Minimal Reproducible Example”和“How To Ask Questions The Smart Way”是我所理解的与开源项目打交道的最基本礼仪。在日常开发中,我认为这两者也是十分必要的,而且能够给解决问题的效率带来质的提升。


Original link:https://izhangzhihao.github.io//2021/06/06/解决-Bug-的正确姿势/

Search

    Table of Contents