迈向零缺陷

2025-05-25

迈向零缺陷

我的个人和职业使命是编写无错误的代码。

零缺陷的软件看似一个雄心勃勃的目标。随着时间的推移,软件缺陷不断增多,甚至变得习以为常,一些开发者和用户甚至对缺陷习以为常。

虽然实现零缺陷很难,但我认为值得一试。我们不应该认输,也不应该提前假设我们的产品会有缺陷。相反,我们应该尽一切努力,避免在软件中无意中产生缺陷,因为这些缺陷是可以避免的。我们越接近零缺陷,就越好!

随着时间的推移,我逐渐在心里构建了一个清单,列出需要注意的事项,既包括我编写的代码,也包括它生成的运行应用程序中的事项,以便识别潜在的错误。现在,每当我即将完成一项变更或新功能时,我都会查看这份清单。我也一直在努力培养一种鼓励自律、严谨和注重细节的思维模式。

通过运行这些检查并建立这种思维方式,我的目标是尽早发现并修复错误,而不是让它们出现在测试环境中,或者更糟糕的是,出现在最终用户面前。

我很乐意与其他开发者分享这一点。请阅读并在评论区告诉我你的想法!

清单

不用多说,以下是我的清单:

拼写错误、意外按键、调试语句。每次准备提交代码时,请稍等片刻,检查一下提交的变更差异。确保只提交你真正想要提交的内容。检查拼写错误、意外按键、大写字母拼写错误等。编译器或 Linter 通常可以发现这些问题,但有时也会遗漏一些情况,所以仍然值得花几秒钟仔细检查一下差异。同时,还要检查仅用于开发的代码,例如日志记录或调试语句,这些代码虽然通过了编译,但不应该被提交。

细微的逻辑错误。查找所有那些乍一看和编译器都认为合理但实际上却错误代码的错误。例如

  • 误报。例如:if (!hidden) { show(); } else { hide(); }。观察到这!hidden实际上相当于可见。所以这段代码实际上会show()在已经可见和hide()不可见时执行!为了纠正这个问题,我们需要删除 ,!并得到类似这样的代码:if (hidden) { show(); } else { hide(); }。密切关注这类微妙的逻辑错误非常重要。
  • 表达式被强制转换为错误的布尔值。例如,在 JavaScript 中,一个indexOf(x)调用应该与数值进行比较,但却没有与任何值进行比较。实现此意图的正确(且更清晰)方法可能是调用includes(x),它返回一个布尔值。
  • 差一错误。例如:for (let i = 0; i <= 10; i++) { ... }。此循环运行了 11 次迭代,而预期运行 10 次。将其重写为:会更清晰for (let i = 0; i < 10; i++) { ... }
  • 过滤操作。您可能执行了过滤功能,但意外地从列表中提取了项目并仅返回了这些项目,而您的意图是返回包含这些项目的完整列表。或者,您的代码可能会返回除某些项目之外的所有内容,而您的意图是即使这些项目存在也不返回任何内容。这种情况还有很多其他变体。总之,请仔细检查复杂的过滤操作。

边缘情况。要找到这些情况,请尝试中断你的应用程序。

  • 快速连续地点击 UI 的许多不同部分。
  • 测试长序列操作,确保最终结果完全符合预期。例如,彻底测试撤消/重做功能,方法是执行一个操作,然后撤消,再重做,如此反复多次,最后验证最终结果。
  • 输入值的数量意外地大,格式意外,或者输入空值。
  • 使用格式正确但不合逻辑的值进行测试(例如,该月 32 日的日期)。
  • 向列表中添加比正常数量更多的项目。
  • 一次运行应用程序的多个实例并验证它是否仍然正常运行。

基本上,尽一切可能让你的应用程序崩溃,并确保它在任何情况下都能优雅地恢复。如果你有大量可能的输入组合需要测试,单元测试绝对是你的好帮手!

值 vs. 引用。您是否希望在一个地方设置一个值,然后在其他多个地方更新它?或者您是否希望在多个地方保存该值的独立副本?请检查您对引用和值的使用情况,并确保它们适合您的用例。

内存泄漏。由于内存分配不正确且不受约束,内存泄漏会显著降低应用程序的运行速度,甚至导致应用程序崩溃。内存泄漏的表现形式多种多样,具体取决于您使用的开发语言和环境。
例如:

  • 在 C# 或 Java 中,它可能是一个未被清理的非托管资源。
  • 在多线程应用程序中,死线程。
  • 在 Javascript 中,引用不再存在的 DOM 节点的 Map。
  • 在 RXJS 中,您忘记取消订阅可观察对象的订阅。

除了手动检查代码之外,几乎每个环境都有一套用于诊断内存泄漏的工具。例如,对于 .NET,有一个内存分析器;对于 JavaScript,大多数浏览器中的开发者工具都有一个“内存”选项卡或类似的功能。

代码执行过于频繁。您是否在 for 循环、游戏循环、模板、渲染周期或代码库中任何其他连续执行多次的部分执行了不必要的操作?这可能会导致应用运行速度变慢,如果情况严重,甚至可能被视为错误行为。一些可能不需要运行的代码包括每次迭代都生成相同结果的代码(在这种情况下,某种形式的缓存会很有帮助),或者仅在某些状态下需要的代码(if在不需要时,围绕该状态的简单语句可以跳过这些代码)。

相同但不同。如果有两个事物外观和行为非常相似,但本质上却存在差异,则要格外小心。我最近就遇到过这种情况,构建两个树形视图时,它们描绘的数据基本相同,但每个树形视图上的视觉标记略有不同。这些视觉标记突出显示了同一数据的两个相反方面。但是,我不小心在其中一个树形视图上编码时颠倒了元素的顺序!这个错误本应显而易见,但我却没有注意到。我太专注于标记的正确性(差异性),以至于忘记确保顺序的正确性(相同性)。回想起来,如果我当时回过头仔细检查最终结果的差异性是正确的,而不是错误的,我就能及早发现并修复它。

空值检查。每当比较两个值时,如果需要,你是否对比较的两端都进行了空值检查和未定义值检查?并且处理了当其中一个或两个都为空时该如何处理?根据需要添加检查。(有些语言为此提供了便利/语法糖。例如,JavaScript 有可选链运算符: ?.。)

异步数据依赖。您的应用是否依赖于多组数据,而这些数据的加载时间可能不同?如果数据加载不完整,会发生什么情况?应用会崩溃吗?或者,它能优雅地处理这种情况,比如等到所有数据加载完毕,并同时显示“正在加载”的提示?您可以使用编程语言的“延迟”机制,临时为某个数据源添加延迟来模拟这种状态。例如,调用 JavaScript 的setTimeout方法、RX 的延迟运算符或 .NET 的Thread.Sleep()。当然,在提交测试代码之前,请务必还原所有测试代码!

浏览器/操作系统升级。根据你正在开发的环境,当新版本发布时,请注意该环境可能出现的重大更改。每当有新版本发布时,请升级浏览器/操作系统,并在新版本中测试你的应用程序,查找错误。最近,我亲身体验了这一点的重要性,Chrome 72 中 Flexbox 的更改导致了一些 CSS 更改。

设备、屏幕尺寸和缩放比例。如有需要,请在多种设备上测试您的应用——移动设备、平板电脑和/或桌面设备。您可能还需要在这些设备上使用多种浏览器以及多种版本和不同规格的设备进行测试。此外,请尝试增加/减少缩放级别,并确保布局、大小等仍然保持比例。

可访问性。在软件应用领域,无障碍功能的错误甚至缺失都是一个主要问题。如果您的应用将面向广大用户,您或许应该确保其具备无障碍性。理想情况下,无障碍功能从一开始就“内置”于应用之中,但这并不意味着无需定期严格测试无障碍功能是否有效。在我自己的无障碍审核中,我主要关注三个方面:A)纯键盘操作;B)非可视化操作;C)遵守 WCAG 标准。这三个方面的基本测试可以在任何网页上进行,具体方法是:A)将鼠标移开,尝试不使用键盘操作应用程序;B)将视线从屏幕上移开,尝试仅使用屏幕阅读器操作应用程序;C)运行Wave自动化测试工具并查看其输出。类似的测试也可以在非 Web/原生应用上运行。由于篇幅较长,我计划撰写一篇专门的文章来探讨这个主题。同时,您可以查看一些优秀的资源,例如WAIEasy Checks页面。

日期和时间的处理和格式化。测试与日期或时间相关的代码时要格外小心。如果代码需要对日期/时间值进行某种计算,请尝试使用各种输入进行测试,并确保它始终能够生成正确的日期/时间结果。此外,还要测试它在不同时区下是否正常工作。要在本地执行此操作,您可以临时更改系统时区,重新加载应用程序,然后重新测试日期/时间功能。

数字值,例如货币。与日期/时间一样,请彻底测试应用程序中任何与数字相关的操作,尤其是特定于语言环境的数字,例如货币值。此外,还要检查是否需要将数字值作为字符串接收,并在使用前将其转换为合适的数字类型。

负载测试。当大量数据通过系统时,系统会崩溃吗?用一个包含数千甚至数百万条记录的虚拟数据源来测试应用程序是否能够处理该负载。

需求 vs. 解决方案。仔细检查原始需求,看看你是否真的解决了它们。语言中可能存在你忽略的细微暗示,或者一些你尚未澄清的歧义。如果你需要回到业务部门澄清这些问题,请尽快尽早进行,这样你就有更好的机会在发布代码之前修复任何错误。

点击刷新。有时,由于我不完全理解(或许也不想理解)的原因,正在运行的应用程序会与生成它的代码不同步。是的,即使使用自动编译工具,这种情况也可能发生。对于 Web 应用,资源缓存可能会发挥作用。对于原生应用,进程可能保持打开状态。我有时会花半个小时甚至更长时间试图找出某些功能无法正常工作或无法重现错误的原因,结果却发现我使用的版本已经过时了。长话短说:如有疑问,请点击重启并刷新。

多环境。大多数组织都有多个环境,软件会以分阶段的方式部署到这些环境中。首先是本地开发机器,然后是开发服务器,然后是预发布和/或质量保证环境,最后是生产/发布/上线环境。最好在每个环境中都对您的应用程序进行一些测试。如果您的功能或变更依赖于特定于环境的因素(例如配置值、数据库模式、数据和其他系统、服务或资源),这一点尤为重要。在新环境中,任何事情都可能出错,从配置值的拼写错误到资源授权缺失。您不必在每个环境中都测试所有内容,但至少测试一下最佳路径可能是个好主意。

查找并修复类似的错误(并将修复方案推广!)。最近,一位同事发现了一个错误,该错误使用了错误的属性来从 HTTP 响应中检索错误消息。我没有只修复这一个响应,而是测试了代码库中所有从 HTTP 响应中检索错误消息的地方,并在必要时进行了修复。之后,我更进一步,将 HTTP 错误处理提取到一个通用函数中,从而推广了该修复方案。这样,不仅消除了其他错误,而且通过改进整个框架,也防止了将来出现类似的错误。

添加错误。添加新代码时,务必小心,避免引发错误。例如,向类添加字段、向枚举添加值等都可能导致意外行为。如果您的代码会动态读取您正在修改的结构,例如使用反射循环遍历类中字段的代码,这一点尤其重要。(这种“动态访问”通常并非最佳实践,但遗憾的是,有些代码库确实使用了这种做法,因此我们可能需要检查正在处理的代码库。)

遗漏错误。添加新代码时,务必注意不要忘记包含某些内容,否则可能会导致错误。假设我们创建了一个可继承类的新子类型,我们可能需要包含某些字段或值。例如,如果我们的代码库中包含一些动态代码,这些代码会循环遍历该类所有子类型中的字段,并期望某些字段存在,那么编译器可能不一定会提示这一点。

在数据源不可用的上下文中使用数据源。当我们从组件调用方法或函数时,我们可能会通过使用该组件并查看其是否正常工作来验证代码是否正常工作。但是,这种调用会在使用该组件的所有可能上下文中都有效吗?如果有其他访问同一组件的方式,而这种调用会中断怎么办?如果我们不了解组件使用的不同上下文,这种情况可能非常微妙且容易被忽略。例如,我在 React 中处理弹出模式时就遇到过这种情况。该模式使用了一个依赖于浏览器 URL 中特定数据的钩子。但我不知道可以从另一个页面使用不同的 URL 访问该模式,而该页面没有该数据。不同的 URL 破坏了钩子,从而破坏了我的模式组件。

合并后重新测试。完成更改并推送后,您可能需要解决合并冲突或变基更改。合并后请务必重新测试您的工作!即使成功自动合并,也可能导致您遗漏的细微逻辑错误。这同样适用于您根据拉取请求注释、构建错误等所做的任何更改。

远程 API 调用。确保代码所依赖的所有远程 API 调用均正常运行。例如 HTTP 请求、Web-Socket 连接等。

心态

这份清单可能看起来令人望而生畏,尤其是在时间紧迫的情况下。然而,您不必对每项更改都执行所有这些操作。我通常会快速浏览一下这份清单,只挑选与我正在进行的更改相关的项目。例如,更改数值计算逻辑可能不需要检查“设备、屏幕尺寸和缩放比例”。同样,对于对话框布局的更改,我可能可以跳过“异步数据依赖项”。

“旧”思维模式(我有时在行业中看到)是:

  • 我假设我的代码默认没有错误。
  • 优秀的开发人员从不编写有错误的代码,所以我不应该过多地检查我的代码是否有错误,否则我可能会发现自己是一个糟糕的开发人员!
  • 没有足够的时间来检查错误,所以我别无选择,只能发送有错误的代码。
  • 随着经验的积累,我的代码自然会变得越来越可靠。
  • 测试和修复错误很无聊、乏味而且没有乐趣。
  • 彻底测试和修复错误不会带来任何回报。
  • 软件开发并不重要,是卑微的“苦差事”,所以即使我们做错了也没关系。

我想传播的“新”思维方式,我认为更有成效,那就是:

  • 除非另有证明,否则我的代码是有缺陷的。
  • 成为一名优秀开发人员的一部分是要有纪律和耐心来检查我编写的代码,这些代码看起来不错 - 甚至很出色 - 并找到并修复我知道可能潜伏在其中的所有错误。
  • 几乎总是需要花费一些额外的时间来付出一些真诚的努力来查找和修复错误。
  • 定期、持续地努力编写可靠的代码将使我的代码更加可靠。
  • 只要保持积极的心态,再加点“游戏化”,测试和修复 bug 就能变得充满乐趣。我可以享受修复 bug 时那种兴奋的快感,并且知道最终的代码比我刚发现时更好。
  • 测试和修复 bug 的回报是锻炼我的精神力量(纪律性、严谨性、对细节的关注等等),从而最终打造出更可靠的软件。这些力量不仅能提升我在 bug 修复方面的水平,还能帮助我在解决问题的各个方面取得进步。此外,我还能树立“可靠软件开发者”的声誉,这对我的职业生涯大有裨益。
  • 软件开发是一种职业,也是一门手艺,我们应该为自己的工作感到自豪。

让千份清单绽放光芒!

你会记录下类似的清单吗?无论是书面的还是心里想的?你还想在这个清单里添加其他内容吗?关于编写可靠、无 bug 代码所需的思维模式,你还有什么要补充的吗?

欢迎在评论区留言分享你的清单和经验,或者分享链接。分享任何可以帮助我们开发者改进、编写无 Bug 代码的想法,都非常棒。

感谢阅读!


一些启发我的资源:

  • 代码完成(史蒂夫·麦康奈尔)
  • 《程序员修炼之道》(安德鲁·亨特、大卫·托马斯著)
  • 整洁代码(鲍勃·马丁)
  • 《清单宣言》(阿图·葛文德)
文章来源:https://dev.to/conw_y/towards-zero-bugs-1bop
PREV
构建 A4 简历以添加到您的网站(并使其可打印) grid me
NEXT
我厌倦了手动研究和网页设计,所以我为它构建了一个人工智能机器人🤖✨