避免间歇性测试

间歇性错误是指测试失败,但发生的方式似乎是间歇性的、随机的。许多此类故障可以通过良好的测试编写原则来避免。此页面试图解释一些供编写测试的人员以及那些审查测试以将其纳入 mozilla-central 的人员使用的原则。

在其他项目中,它们也被称为不稳定的测试。

接下来列出了一些已知会导致间歇性故障的模式,并说明了每个模式导致测试失败的原因以及如何避免。

在编写成功的测试用例后,请确保在本地运行它,最好是在调试版本中。也许测试依赖于另一个测试的状态或某些未来的测试或浏览器操作来清理剩余内容。这是浏览器 chrome 中的一个常见问题,以下是一些尝试的方法

  • 调试模式,单独运行测试 ./mach <test> <path>/<to>/<test>/test.html|js

  • 调试模式,单独运行测试目录 ./mach <test> <path>/<to>/<test>

  • 调试模式,单独运行测试更大的目录 ./mach <test> <path>/<to>

过早访问 DOM 元素

data: URL 异步加载。在尝试访问子文档的 DOM 之前,您应该等待加载 <iframe> 加载 data: URL 的加载事件。

例如,以下代码模式很糟糕

<html>
  <body>
    <iframe id="x" src="data:text/html,<div id='y'>"></iframe>
    <script>
      var elem = document.getElementById("x").
                          contentDocument.
                          getElementById("y"); // might fail
      // ...
    </script>
  </body>
</html>

相反,请按如下方式编写代码

<html>
  <body>
    <script>
        function onLoad() {
          var elem = this.contentDocument.
                          getElementById("y");
          // ...
        };
    </script>
   <iframe src="data:text/html,<div id='y'>"
           onload="onLoad()"></iframe>
  </body>
</html>

在定义之前使用脚本函数

这可能与事件处理程序最相关。假设您有一个 <iframe>,并且您希望在它加载后执行某些操作,因此您可能会编写如下代码

<iframe src="..." onload="onLoad()"></iframe>
<script>
  function onLoad() { // oops, too late!
    // ...
  }
</script>

这很糟糕,因为 <iframe> 的加载可能在脚本解析之前完成,因此在 onLoad 函数存在之前完成。这会导致您错过 <iframe> 加载,这可能会导致您的测试超时,例如。解决此问题的最佳方法是将函数定义移动到其在 DOM 中使用的位置之前,如下所示

<script>
  function onLoad() {
    // ...
  }
</script>
<iframe src="..." onload="onLoad()"></iframe>

依赖异步操作的顺序

通常,当您有两个异步操作时,您不能假设它们之间有任何顺序。例如,假设您有两个 <iframe>,如下所示

<script>
  var f1Doc;
  function f1Loaded() {
    f1Doc = document.getElementById("f1").contentDocument;
  }
  function f2Loaded() {
    var elem = f1Doc.getElementById("foo"); // oops, f1Doc might not be set yet!
  }
</script>
<iframe id="f1" src="..." onload="f1Loaded()"></iframe>
<iframe id="f2" src="..." onload="f2Loaded()"></iframe>

此代码隐式地假设 f1 将在 f2 之前加载,但此假设是不正确的。一个简单的解决方法是检测所有异步操作何时完成,然后执行您需要执行的操作,如下所示

<script>
  var f1Doc, loadCounter = 0;
  function process() {
    var elem = f1Doc.getElementById("foo");
  }
  function f1Loaded() {
    f1Doc = document.getElementById("f1").contentDocument;
    if (++loadCounter == 2) process();
  }
  function f2Loaded() {
    if (++loadCounter == 2) process();
  }
</script>
<iframe id="f1" src="..." onload="f1Loaded()"></iframe>
<iframe id="f2" src="..." onload="f2Loaded()"></iframe>

使用神奇的超时来导致延迟

有时,当正在进行异步操作时,您可能会尝试使用超时等待一段时间,希望操作到那时已经完成,并且可以安全地继续。此类代码使用如下模式

setTimeout(handler, 500);

这应该在您的脑海中敲响警钟。一旦您看到此类代码,您应该问问自己:“为什么是 500 而不是 100?为什么不是 1000?为什么不是 328?”您永远无法回答这个问题,因此您应该始终避免此类代码!

此代码的问题在于,您假设 500 毫秒足以完成您正在等待的操作。这可能会根据平台、运行此代码的 Firefox 调试版本或优化版本、机器负载、测试是否在虚拟机上运行等而不再成立。并且它迟早会开始失败。

与其使用此类代码,不如显式地等待操作完成。大多数情况下,可以通过侦听事件来完成此操作。有时没有合适的事件可以侦听,在这种情况下,您可以将一个事件添加到负责完成手头任务的代码中。

理想情况下,神奇的超时永远不需要,但有几种情况,特别是在编写 web-platform-tests 时,您可能需要它们。在这种情况下,请考虑记录使用计时器的原因,以便如果将来证明不再需要,可以将其删除。

在不考虑对象可能被销毁的情况下使用对象

这是我们的测试套件中非常常见的模式,最近发现它会导致许多间歇性故障

function runLater(func) {
  var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  timer.initWithCallback(func, 0, Ci.nsITimer.TYPE_ONE_SHOT);
}

此代码的问题在于,它假设 timer 对象的生存时间足够长,以便计时器触发。如果在计时器需要触发之前执行了垃圾回收,则可能并非如此。如果发生这种情况,timer 对象将被垃圾回收并消失,而计时器还没有机会触发。解决此问题的一个简单方法是使 timer 对象成为全局对象,以便在垃圾回收代码尝试回收它时仍然存在对该对象的未决引用。

var timer;
function runLater(func) {
  timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  timer.initWithCallback(func, 0, Ci.nsITimer.TYPE_ONE_SHOT);
}

类似的问题可能发生在传递给 nsIWebProgress.addProgressListener() 方法的 nsIWebProgressListener 对象上,因为 Web 进度对象存储对 nsIWebProgressListener 对象的弱引用,这不会阻止它被垃圾回收。

需要焦点的测试

某些测试需要应用程序窗口获得焦点才能正常运行。

例如,如果您正在编写崩溃测试或重新测试以测试获得焦点的元素,则需要在清单文件中指定它,如下所示

needs-focus load my-crashtest.html
needs-focus == my-reftest.html my-reftest-ref.html

此外,如果您正在编写使用 synthesizeKey() 合成键盘事件的 mochitest,则窗口需要获得焦点,否则测试会在 Linux 上间歇性地失败。您可以通过使用 SimpleTest.waitForFocus() 并从该函数的回调内部开始测试的操作来确保这一点,如下所示

SimpleTest.waitForFocus(function() {
  synthesizeKey("x", {});
  // ...
});

需要鼠标交互、打开上下文菜单等的测试也可能需要焦点。请注意,waitForFocus 也隐式地等待加载事件,因此对于尚未完成加载的窗口,可以安全地调用它。

执行时间过长的测试

有时,单个单元测试中发生的事情太多了。如果正在运行的机器负载过重,这会导致测试在其执行期间在随机位置超时,这表明测试需要更多时间才能执行。这可能仅在调试版本中发生,因为调试版本通常速度较慢。有两种方法可以解决此问题。其中一种是将测试拆分为多个较小的测试(这可能还有其他优点,包括更好的可读性),或者要求测试运行程序框架为测试提供更多时间以正确完成。后者可以使用 requestLongerTimeout 函数完成。

未正确清理的测试

有时,测试会为各种事件注册事件处理程序,但它们没有正确地进行清理。或者,有时测试会执行对运行测试套件的浏览器产生持久影响的操作。示例包括打开新窗口、添加书签、更改首选项的值等。

在这些情况下,有时会在测试检入树中后立即捕获问题。但未正确清理的内容也可能对未来的(可能看起来不相关的)测试产生间歇性影响。这些类型的间歇性故障可能非常难以调试,并且一开始并不明显,因为大多数人只查看发生故障的测试,而不是之前的测试。故障的外观因情况而异,但其中一个示例是 错误 612625

未等待所需的特定事件

有时,测试会等待事件 B 而不是等待事件 A,隐式地希望 B 发生意味着 A 也已发生。错误 626168 就是一个例子。测试确实需要等待执行过程中的绘制,但它会等待事件循环命中,希望在我们命中事件循环时,绘制也已发生。虽然这些类型的假设在开发测试时可能成立,但不能保证每次运行测试时都成立。在编写测试时,如果您必须等待事件,则需要注意等待事件的原因以及您究竟在等待什么,然后确保您确实在等待正确的事件。

依赖外部站点的测试

即使外部站点实际上没有宕机,外部站点的性能变化和外部网络也会为测试持续时间增加足够的差异,很容易导致测试间歇性地失败。

不应将外部站点用于测试。

依赖 Math.random() 生成唯一值的测试

有时,您需要在测试中使用唯一值。使用 Math.random() 获取唯一值在大多数情况下都能奏效,但此函数实际上并不能保证其返回值是唯一的,因此您的测试可能会从该函数获得重复的值,这意味着它可能会间歇性地失败。如果您需要必须对测试唯一的唯一值,则可以使用以下模式代替调用 Math.random()

var gUniqueCounter = 0;
function generateUniqueValues() {
  return Date.now() + "-" + (++gUniqueCounter);
}

依赖当前时间的测试

在编写依赖于当前时间的测试时,应格外注意测试运行时不同类型的行为。例如,您的测试如何处理测试运行期间夏令时 (DST) 设置更改的情况?如果您正在测试相对于当前时间的时间概念(例如今天、昨天、明天等),您的测试是否处理这些概念在其测试过程中改变其含义的情况(例如,如果您的测试在给定日期的 23:59:36 开始并在 00:01:13 结束)?

依赖于时间差异或比较的测试

在进行时间差异时,应考虑操作系统的计时器分辨率。例如,连续调用Date()不能保证获得不同的值。此外,在跨越 XPCOM 时,不同的时间实现可能会产生令人惊讶的结果。例如,当比较通过PR_Now获得的时间戳与通过 JavaScript 日期获得的时间戳时,最后一个调用可能导致第一个调用的过去!这些差异在 Windows 上更为明显,偏差可能高达 16 毫秒。在全球范围内,计时器的分辨率是猜测,不能保证(也由于虚拟机上的虚假分辨率),因此当真正需要比较时,最好使用更大的范围。

销毁原始标签的测试

从浏览器 chrome 测试窗口中删除原始标签的测试可能会导致间歇性橙色错误,或者本身可能是间歇性橙色错误。显然,这两种结果都是不可取的。您既不应该编写执行此操作的测试,也不应该 r+ 执行此操作的测试。作为一般规则,如果您在测试清理代码中调用addTab或其他标签打开方法,那么您可能正在做一些不应该做的事情。