http://testerhome.com/topics/577

原文请见 Minimizing Unreproducible Bugs

不能重现的 bug 是我的灾难。我常常找到一个bug 后来又听说这不是一个 bug,因为它无法重现。但是这个 bug 仍旧在那里,等着捕食下一个受害者。这些类型的 bug 非常昂贵,因为我们需要花大量的时间去调查。它们也会对产品体验造成破坏性的影响,特别是用户发现并报告了这些被忽略的 bug。所以为了防止这类问题,我们需要做更多。在这篇文章里,我将探讨一些明显或者不是那么明显的开发或者测试的准则,这些准则能多少减少些这些 bug 发生的可能性。

如何避免或者测试竞争,死锁,内存崩溃,时间问题,访问未初始化内存,内存泄露,资源问题

在这一节里我会将许多类型的 bug 混在一起说。这些 bug 在我们如何测试它们和它们难以重现和调试这点上是相关的。
其根源及其影响可以是微秒级别,也可能持续数个小时。它们的堆栈信息可能不存在,也可能会误导人。当遇到不正常的流量高峰或者资源不够的情况下,系统可能会出现怪异的运行故障。单一的访问模式或者资源配置会导致多线/进程竞争或者死锁。当很多组件集成一起,而各组件的性能参数差异和失败/重启/超时时间延迟会让系统一团糟,这时候就会出现时间同步问题。我们在大量的函数调用里可能不会注意到内存崩溃或者访问未初始化内存的问题,但是在一些边缘用例下就很致命。一般只有在负载或者长时间运行,内存泄露才会被发现。

开发准则:

  • 简化你的同步逻辑。如果你的逻辑理解起来非常困难,那么要重现和调试复杂的并发问题也变得非常困难。
  • 使用相同的顺序取锁,来避免死锁。这是一个实践出真知的准则。但是我依然能偶尔看到不遵守这个准则的代码。定义一个顺序去拿锁,并保持这个顺序。
  • 不要通过创建多个细粒度锁来优化,除非你确信你需要这么做。额外的锁只会增加并发复杂性。
  • 避免共享内存,除非你真的需要它。访问共享内存很容易出错。但是这类 bug 却很难重现。

测试准则:

  • 有规律地对你的系统进行压力测试。不要惊讶,你的系统在高压下肯定会有出乎意料的故障。
  • 超时测试。模拟或者伪造依赖来测试超时的代码。如果你的超时代码有问题,在某种系统情况下,它可能会导致 bug。
  • 要对调试的版本和优化过的版本都进行测试。你可能会发现一个表现很好的调试版本,工作的没有任何问题,但是一旦经过优化,就出现奇怪的系统故障。
  • 在资源受限的情况下测试。试着减少数据中心,机器,进程,线程的数量,减少硬盘空间或者内存。也试试看模拟差的网络环境。
  • 耐久性测试。有些 bug 需要长时间运行才能暴露出来。比如,持久化数据迟早会崩溃。
  • 使用动态分析工具,比如内存调试器,ASan,TSan 和 MSan。他们可以帮助定位很多类型的无法重现的内存/线程问题。

强制实施先决条件

我见过很多有着高容错性的善意函数。比如,看下面的这个函数:

void ScheduleEvent(int timeDurationMilliseconds) {
if (timeDurationMilliseconds <= 0) {
timeDurationMilliseconds = 1;
}
...
}

当输入 timeDurationMilliseconds 不合理时,函数可以调整输入为可接受的值,但是它也可能掩盖了一个 bug。调用代码可能会遇到本文中描述的任一问题,而且即使传递垃圾数据给这个函数也能正常工作(只要不小于等于0)。这种带有容错的函数越多,想要找到错误根源就越难,而且很有可能,最终用户也会看见这些垃圾信息。强制实施先决条件,比如用断言,对于新系统而言,的确可能会导致很多的故障失败。但是随着系统的成熟,可以及早发现很多小的大的问题。这些检查可以帮助提高系统的长期可靠性。

开发准则:

在你的函数里强制实施先决条件,除非你有更好的理由不去做。

使用防御性编程

防御性编程是另一个靠得住的技术,它能够有效的减少无法重现的 bug。如果你的代码使用依赖去做一些事情,但是依赖的代码执行失败却没有抛出错误或者返回了垃圾,你的代码该如何处理?你可以通过模拟或者伪造来测试这种情景。但是或许你在代码对它的依赖做一些健全检查会更好。比如:

double GetMonthlyLoanPayment() {
double rate = GetTodaysInterestRateFromExternalSystem();
if (rate < 0.001 || rate > 0.5) {
throw BadInterestRate(rate);
}
...

开发准则:

尽可能的使用防御性编程,验证你所依赖部分的工作,尤其是会造成故障的已知风险,比如用户提供的数据,I/O操作和 RPC 调用。

测试准则:

使用 fuzz testing 来测试系统容错的健壮性。

不要从用户角度处理所有的错误问题

近年来有一种趋势,不惜任何代价不然用户看到故障。这在很多案例中,很有意义。但是在一些案例中,我们过头了。
如果用户遇到小故障,但是代码没有抛出异常或者直接放过了,那么无知的用户会在一个失败的状态下继续工作。到最后,软件总会到一个致命的点,所有造成这个致命故障的原因都被忽略了。如果用户不了解先前的错误,他们就不能报告这些错误,你也没办法重现它们。

开发准则:

  • 只有当你确定不会对系统状态或者对用户产生影响的时候,对用户隐藏错误。
  • 任何对用户有影响的错误都应该报告给用户,并告诉用户如何处理。向用户显示的信息,连同向工程师展示的数据,应该足够判断到底哪里出错了。

测试出错处理

错误处理的代码是最常不会被测试的部分。测试覆盖不应该略过这里的。如果不能非常好地处理致命错误,糟糕的错误处理代码会造成不能重现的 bug 并带来风险。

测试准则:

  • 测试你的错误处理代码。最好的方法是模拟或者伪造能触发错误的组件。
  • 通过日志检查所有类型的错误处理。

检查重复的键

如果唯一标识或者访问数据的键是通过随机生成的,不能保证全局唯一性的话,重复的键会导致数据损坏或者并发问题。这种问题非常难重现。

开发准则:

  • 尽力保证所有键的唯一性。
  • 如果不能保证的话,在使用前先检查键是否试用过。
  • 小心潜在的竞争,避免它们同步。

测试并发数据访问

有些 bug 只会在多个客户端读写同一块数据时候发生。一般压力测试会覆盖这些用例,但是如果没有的话,你就需要特别设计一些并发数据访问的用例。这种情况下发生的 bug 常常是无法重现的。比如,一个用户可能有两个应用实例运行在同一个账户上,它们可能没有注意到这点,直到错误发生。

测试准则:

  • 如果并发数据访问是系统的特性,那一定要测试它。事实上,就算不是系统的特性,也需要验证系统是否存在并发访问数据问题。测试并发非常有挑战。我常用的方法是创建许多工作者线程同时尝试访问,主线程则监控和验证哪些尝试是真正并发的,像预期那样阻塞或者被允许的,或者全部成功了。为了确保系统工作正常,代码层面上,对所有尝试和改变系统状态行为的后续分析也是必须的。

绕开不明确的行为和不明确的数据访问

当在某些状态下或者某些输入下,一些 API 和基本操作会对未定义的行为报警。同样,一些数据结构没有办法保证迭代顺序(比如 JAVA 的 Set)。在代码里忽略这些警告大多数时候能很好的工作。但是一旦失败,就很难重现。

开发准则:

你要了解你所使用的 API 和操作可能有未定义的行为,需要预防这些情况。不要过多依赖数据结构的迭代顺序,除非你能保证这个顺序。依赖集合和关联数组的顺序是比较普遍的错误。

记录错误日志故障日志的细节

如果日志包含足够的出错细节,那么本文描述的问题就可以很容易地重现和调试。

开发准则:

  • 遵从良好的日志实践,特别在出错处理代码里。
  • 如果日志保存在用户的机器,提供一个便捷的方法,让用户提供给你日志文件。

测试准则:

保存你的日志以便后续分析。

还有什么要添加?

我有遗漏什么重要的,可以减少这些 bug 的准则吗? 你发现和解决的难以重现的 bug 是什么?

[ZZ]最小化不可重现的bug的更多相关文章

  1. qt widget设置Qt::FramelessWindowHint和Qt::WA_TranslucentBackground, 会出现一个bug: 在最小化后还原时界面停止刷新

    qt widget设置Qt::FramelessWindowHint和Qt::WA_TranslucentBackground, 会出现一个bug: 在最小化后还原时界面停止刷新 Widget wit ...

  2. Directx11教程(21) 修正程序最小化异常bug

    原文:Directx11教程(21) 修正程序最小化异常bug       很长时间竟然没有注意到,窗口最小化时候,程序会异常,今天调试水面程序时,随意间最小化了窗口,发现程序异常了.经过调试,原来程 ...

  3. 实现iOS图片等资源文件的热更新化(四): 一个最小化的补丁更新逻辑

    简介 以前写过一个补丁更新的文章,此处会做一个更精简的最小化实现,以便于集成.为了使逻辑具有通用性,将剥离对AFNetworking和ReativeCocoa的依赖.原来的文章,可以先看这里: htt ...

  4. dfa最小化,修正了上个版本的一些错误。

    上个版本测试的时候,只用了两个非常简单的测试用例,所以好多情况有问题却没有测试出来 bug1:在生成diff_matrix的时候,循环变量少循环了一次,导致最后一个节点在如果无法与其他点合并的情况下, ...

  5. Effective Java 第三版——57. 最小化局部变量的作用域

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  6. Ubuntu GNOME单击任务栏图标最小化设置

    在Ubuntu GNOME的发行版中,桌面使用的是GNOME,GNOME可以像Windows那样有一个底部任务栏,在Ubuntu GNOME中它称为 dash to dock,如下图: Windows ...

  7. Swing手动进行最大化最小化

    首先jdk的setExtendedState是有bug的,需要先重载JFrame的setExtendedState方法 /** * Fix the bug "jframe undecorat ...

  8. C++ 最小化到托盘

    #define WM_SHOWTASK (WM_USER + 1) void CTestDlg::OnSysCommand(UINT nID, LPARAM lParam) { if ((nID &a ...

  9. nw.js自定义最小化图标的click事件

    选择frameless时,最小化和关闭按钮的点击事件需要自己来做,办法是: /* * 下面两个模块一定要引入到js文件中 */ var gui = require('nw.gui'); var win ...

随机推荐

  1. PHP生成图片验证码demo【OOP面向对象版本】

    下面是我今天下午用PHP写的一个生成图片验证码demo,仅供参考. 这个demo总共分为4个文件,具体代码如下: 1.code.html中的代码: <!doctype html> < ...

  2. [小北De编程手记] : Lesson 08 - Selenium For C# 之 PageFactory & 团队构建

    本文想跟大家分享的是Selenium对PageObject模式的支持和自动化测试团队的构建.<Selenium For C#>系列的文章写到这里已经接近尾声了,如果之前的文章你是一篇篇的读 ...

  3. C#生成条形码 Code128算法

    条形有很多种,Code128是比较常用的一种,是一种高密度条码, CODE128 码可表示从 ASCII 0 到ASCII 127 共128个字符,故称128码.其中包含了数字.字母和符号字符. Co ...

  4. Log4net中的调错

    在使用log4net时,感觉最麻烦的就是配置文件了,为了使用方便,我不得不先准备好一个完整的配置文件方案,测试了输出到文本.控制台.windows事件.SQL Server数据库都没有问题,但输出到o ...

  5. RGui的http代理设置

    办公电脑环境需要http代理访问大网,使用R语言安装包时老是无法连接网络,后来从网上发现解决方法很简单,只需在启动RGui.exe的命令行上加上启动参数就可以了. "C:\Program F ...

  6. Effective Java 阅读笔记——方法

    38:检查参数的有效性 每当编写方法或者构造器的时候,应该考虑它的参数有哪些限制,在方法的开头处对参数进行检查,并且把这些限制写入文档. 注意: 对于公有方法,应该使用@throws标签在文档中说明违 ...

  7. HTTP通信过程底层实现原理

  8. iOS开发之保存照片到自己创建的相簿

    iOS开发之保存照片到自己创建的相簿 保存照片还可以用ALAssetsLibrary,ALAssetsLibrary提供了我们对iOS设备中的相片.视频的访问,是连接应用程序和相册之间访问的一个桥梁. ...

  9. Objective-C之代理设计模式小实例

    *:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...

  10. C语言的传值与传址调用

    传值调用并不能改变两个变量的值,而传址能. 为什么,因为,传值调用,仅仅是在函数内,调换参数的值. 而地址所指向的值,改变的不仅仅是函数内,函数外也改变. 请看代码: 这里还要注意:通常我们不会返回局 ...