XPCShell 测试

xpcshell 测试是运行速度很快的测试,通常用于编写单元测试。它们无法像 browser chrome tests 那样访问完整的浏览器 chrome,因此开销要低得多。通常使用 ./mach xpcshell-test 运行它们,这会使用 xpcshell 测试工具启动一个新的 xpcshell 会话。任何可通过可脚本化接口访问 XPCOM 层的内容都可以使用 xpcshell 进行测试。请参阅 Mozilla automated testingpages tagged "automated testing" 以获取更多信息。

介绍 xpcshell 测试

xpcshell 测试文件名必须以 test_ 开头。

创建新的测试目录

如果您需要创建新的测试目录,请按照此处的步骤操作。测试运行器需要了解测试的存在方式以及如何通过 xpcshell.toml 清单文件对其进行配置。

首先将 XPCSHELL_TESTS_MANIFESTS += ['xpcshell.toml'] 声明(使用正确的相对 xpcshell.toml 路径)添加到位于目录中或上方的 moz.build 文件中。

然后创建一个空的 xpcshell.toml 文件,以告知构建系统有关各个测试的信息,并提供任何其他配置选项。

在现有目录中创建新的测试

如果您在现有目录中创建新的测试,则只需运行

$ ./mach addtest path/to/test/test_example.js
$ hg add path/to/test/test_example.js

这将自动创建测试文件并将其添加到 xpcshell.toml 中,第二行将其添加到您的提交中。

测试文件包含一个空测试,它将让您了解如何编写测试。mozilla-central 中还有更多示例。

运行测试

要运行测试,请从 Gecko 源代码目录的根目录执行 mach 命令。

# Run a single test:
$ ./mach xpcshell-test path/to/tests/test_example.js

# Test an entire test suite in a folder:
$ ./mach xpcshell-test path/to/tests/

# Or run any type of test, including both xpcshell and browser chrome tests:
$ ./mach test path/to/tests/test_example.js

测试由测试工具运行。它将依次调用

  • run_test(如果存在)。

  • 任何使用 add_taskadd_test 添加的函数,按照它们在文件中定义的顺序。

另请参阅下面关于 add_taskadd_test 的说明。

xpcshell 测试 API

xpcshell 测试可以访问以下函数。它们在 testing/xpcshell/head.jstesting/modules/Assert.sys.mjs 中定义。

断言

  • Assert.ok(truthyOrFalsy[, message])

  • Assert.equal(actual, expected[, message])

  • Assert.notEqual(actual, expected[, message])

  • Assert.deepEqual(actual, expected[, message])

  • Assert.notDeepEqual(actual, expected[, message])

  • Assert.strictEqual(actual, expected[, message])

  • Assert.notStrictEqual(actual, expected[, message])

  • Assert.rejects(actual, expected[, message])

  • Assert.greater(actual, expected[, message])

  • Assert.greaterOrEqual(actual, expected[, message])

  • Assert.less(actual, expected[, message])

  • Assert.lessOrEqual(actual, expected[, message])

这些断言方法由 testing/modules/Assert.sys.mjs 提供。它实现了 CommonJS 单元测试规范版本 1.1,该规范为在代码中执行逻辑断言(可选)提供了基本且标准化的接口,并提供可自定义的错误报告。强烈建议使用这些断言方法,而不是下面提到的方法。您可以在所有这些方法中删除名称开头的 Assert.,例如 ok(true) 而不是 Assert.ok(true),但是保留 Assert. 前缀可能被视为更具描述性,并且更容易发现测试在哪里。 Assert.throws(callback, expectedException[, message]) Assert.throws(callback[, message]) 断言提供的回调函数会抛出异常。 expectedException 参数可以是 Error 实例,也可以是与错误消息部分匹配的正则表达式(如在 Assert.throws(() => a.b, /is not defined/ 中)。 Assert.rejects(promise, expectedException[, message]) 断言提供的 Promise 被拒绝。注意:这应该在 await 前缀下调用。 expectedException 参数可以是 Error 实例,也可以是与错误消息部分匹配的正则表达式。示例: await Assert.rejects(myPromise, /bad response/);

测试用例注册和执行

add_task([condition, ]testFunc)

将异步函数添加到要异步运行的测试列表中。每当函数 await 一个 Promise 时,测试运行器都会等待 Promise 解析或拒绝,然后再继续执行。被拒绝的 Promise 会转换为异常,解析的 Promise 会转换为值。您可以选择指定一个条件,该条件会导致测试函数被跳过;有关详细信息,请参阅 通过 add_task 或 add_test 函数添加条件。对于使用 add_task() 的测试, run_test() 函数是可选的,但如果存在,它也应该调用 run_next_test() 以开始执行所有异步测试函数。测试用例不得调用 run_next_test(),当任务完成时会自动调用它。有关更多信息,请参阅下面的 异步测试

add_test([condition, ]testFunction)

将测试函数添加到要异步运行的测试列表中。您可以选择指定一个条件,该条件会导致测试函数被跳过;有关详细信息,请参阅 通过 add_task 或 add_test 函数添加条件。每个测试函数都必须在完成时调用 run_next_test()。对于使用 add_test() 的测试, the run_test() 函数是可选的,但如果存在,它也应该调用 run_next_test() 以开始执行所有异步测试函数。在大多数情况下,您应该使用更易读的变体 add_task()。有关更多信息,请参阅下面的 异步测试

run_next_test()

从异步测试列表中运行下一个测试函数。每个测试函数在完成时必须调用run_next_test()run_test()也应该调用run_next_test()以启动所有异步测试函数的执行。有关更多信息,请参见下面的异步测试

``registerCleanupFunction``(callback)

在当前 JS 测试文件运行完成后执行函数callback,无论其中的测试通过或失败。您可以使用它来清理任何可能在测试运行之间导致问题的任何内容。如果callback返回一个Promise,则测试将不会完成,直到 promise 完成或拒绝(使终止函数异步)。清理函数按注册的反序调用。

do_test_pending()

延迟测试退出,直到调用 do_test_finished()。可以多次调用 do_test_pending(),并且在单元测试退出之前,必须与每个 do_test_finished() 配对。

do_test_finished()

调用此函数以通知测试框架异步操作已完成。如果所有异步操作都已完成(即,每个 do_test_pending() 都已与执行中的 do_test_finished() 匹配),则单元测试将退出。

环境

do_get_file(testdirRelativePath, allowNonexistent)

返回一个表示测试目录中给定文件(或目录)的nsILocalFile对象。例如,如果您的测试是 unit/test_something.js,并且您需要访问 unit/data/somefile,则您将调用do_get_file('data/somefile')。给定的路径必须用正斜杠分隔。如果您的测试需要访问外部文件,则可以使用此方法访问特定于测试的辅助文件。请注意,您还可以使用此函数获取目录。

注意

注意:如果您的测试需要访问测试目录中不存在的一个或多个文件,则应在您指定XPCSHELL_TESTS的 Makefile 中将这些文件安装到测试目录中。例如,请参见netwerk/test/Makefile.in#117

do_get_profile()

向配置文件服务注册一个目录,并返回一个表示该目录的nsILocalFile对象。它还确保在测试结束之前发送profile-change-net-teardownprofile-change-teardownprofile-before-change观察者通知。如果测试中加载的组件观察它们以在关闭时进行清理(例如,位置),这将非常有用。

注意

注意:do_register_cleanup将在观察者通知关闭配置文件和网络之前执行任何清理操作。

do_get_idle()

默认情况下,xpcshell 测试将禁用空闲服务,以便空闲时间始终报告为 0。调用此函数将重新启用服务并返回其句柄;然后将正确地向底层操作系统请求空闲时间。请求空闲服务时可能会触发 idle-daily 通知。如果测试必须使用空闲,建议始终通过此方法获取服务。

do_get_cwd()

返回一个表示测试目录的nsILocalFile对象。这是当前正在运行测试文件时包含测试文件的目录。您的测试可以写入此目录以及读取与您的测试位于同一目录下的任何文件。但是,您的测试应谨慎确保在它打算写入的文件已存在时不会失败。

load(testdirRelativePath)

testdirRelativePath引用的 JavaScript 文件导入到全局脚本上下文中,并在其中执行代码。指定的文件是测试目录中的文件。例如,如果您的测试是 unit/test_something.js,并且您还有另一个文件 unit/extra_helpers.js,则可以通过调用load('extra_helpers.js')从第一个文件中加载第二个文件。

实用程序

do_parse_document(path, type)

解析并返回一个 DOM 文档。

executeSoon(callback)

在稍后遍历事件循环时执行函数callback。当您希望某些代码在当前函数执行完成后执行时使用,但您不关心特定的时间延迟。此函数将自动为您插入do_test_pending / do_test_finished对。

do_timeout(delay, fun)

调用此函数以安排超时。在指定的延迟(以毫秒为单位)后,将调用给定的函数,不提供任何参数。请注意,您必须调用do_test_pending,以便在计时器触发之前不会完成测试,并且如果您的其他功能没有要测试的功能,则必须在超时中执行的操作完成后调用do_test_finished。(注意:用于传递给 eval 的函数参数曾经是一个字符串参数,一些较旧的分支仅支持字符串参数或同时支持字符串和函数。)

多进程通信

do_send_remote_message(name, optionalData)

异步地向所有远程进程发送消息。与do_await_remote_message或等效的 ProcessMessageManager 监听器配对。

do_await_remote_message(name, optionalCallback)

返回一个在收到消息时解析的 promise。必须与do_send_remote_message或等效的 ProcessMessageManager 调用配对。如果提供了optionalCallback,则回调必须调用do_test_finished。如果将 optionalData 传递给do_send_remote_message,则该数据是optionalCallback的第一个参数或 promise 解析到的值。

xpcshell.toml 清单

清单控制测试套件中包含哪些测试以及测试的配置。它是通过 `moz.build` 属性配置属性加载的。

以下是清单的[DEFAULT]部分中列出的测试套件的所有配置选项。

标签

运行多个测试时,可以通过标签过滤测试。mach 的命令是./mach xpcshell-test --tag TAGNAME

头部

头部 JavaScript 文件的相对路径,该文件在运行测试套件之前运行一次。在根作用域中声明的变量作为全局变量在测试文件中可用。有关更多信息和用法,请参见测试头部和支持文件

firefox-appdir

如果您的测试需要访问 browser/ 目录中的内容(例如,位于那里的其他 XPCOM 服务),请将其设置为“browser”。

skip-if run-if fail-if

对于整个测试套件,仅当测试满足某些条件时才运行测试。有关如何使用这些属性,请参见在 xpcshell.toml 清单中添加条件

支持文件

通过resource://test/[filename]路径使文件可供测试使用。路径可以相对于其他目录,但仅使用文件名提供服务。有关更多信息和用法,请参见测试头部和支持文件

[test_*]

测试文件名必须以test_开头,并用方括号列出

创建新的 xpcshell.toml 文件

在创建新的目录和新的 xpcshell.toml 清单文件时,必须将其添加到目录层次结构中该文件附近的 moz.build 文件中

XPCSHELL_TESTS_MANIFESTS += ['path/to/xpcshell.toml']

通常,包含XPCSHELL_TESTS_MANIFESTS的 moz.build 不在与xpcshell.toml相同的目录中,而是在父目录中。常见的目录结构如下所示

feature
├──moz.build
└──tests/xpcshell
   └──xpcshell.toml

# or

feature
├──moz.build
└──tests
   ├──moz.build
   └──xpcshell
      └──xpcshell.toml

测试头部和支持文件

通常在测试套件中,需要在每个测试中加载类似的设置代码和依赖项。这可以通过测试头部完成,测试头部是在xpcshell.toml清单文件中head属性下声明的文件。文件本身通常称为head.js。在测试头部声明的任何变量都将在该测试套件中每个测试的全局作用域中。

除了测试头部之外,其他支持文件也可以在xpcshell.toml清单文件中声明。这是通过support-files声明完成的。这些文件将通过 urlresource://test加上文件名提供。然后可以使用ChromeUtils.import函数或其他加载器加载这些文件。支持文件也可以位于其他目录中,并且可以通过其文件名提供。

# File structure:

path/to/tests
├──head.js
├──module.mjs
├──moz.build
├──test_example.js
└──xpcshell.toml
# xpcshell.toml
[DEFAULT]
head = head.js
support-files =
  ./module.mjs
  ../../some/other/file.js
[test_component_state.js]
// head.js
var globalValue = "A global value.";

// Import support-files.
const { foo } = ChromeUtils.import("resource://test/module.mjs");
const { bar } = ChromeUtils.import("resource://test/file.mjs");
// test_example.js
function run_test() {
  equal(globalValue, "A global value.", "Declarations in head.js can be accessed");
}

其他测试注意事项

异步测试

异步测试(即,在run_test完成之后才能确定其成功与否的测试)可以用多种方式编写。

基于任务的异步测试

最简单的方法是使用add_task帮助程序。add_task可以将异步函数作为参数。add_task测试如果您的代码没有run_test函数,则会自动运行。

add_task(async function test_foo() {
  let foo = await makeFoo(); // makeFoo() returns a Promise<foo>
  equal(foo, expectedFoo, "Should have received the expected object");
});

add_task(async function test_bar() {
  let foo = await makeBar(); // makeBar() returns a Promise<bar>
  Assert.equal(bar, expectedBar, "Should have received the expected object");
});

基于回调的异步测试

您还可以使用add_test,它接受一个函数并将其添加到异步运行函数的列表中。提供给add_test的每个函数也必须在其末尾调用run_next_test。您通常应该使用add_task而不是add_test,但您可能会在现有测试中看到add_test

add_test(function test_foo() {
  makeFoo(function callback(foo) { // makeFoo invokes a callback<foo> once completed
    equal(foo, expectedFoo);
    run_next_test();
  });
});

add_test(function test_bar() {
  makeBar(function callback(bar) {
    equal(bar, expectedBar);
    run_next_test();
  });
});

其他测试

我们还可以告诉测试框架在run_test()完成后不要杀死测试进程,而是继续旋转事件循环,直到我们的回调被调用并且我们的测试完成。较新的测试更喜欢使用add_task而不是此方法。这可以通过do_test_pending()do_test_finished()实现

function run_test() {
  // Tell the harness to keep spinning the event loop at least
  // until the next do_test_finished() call.
  do_test_pending();

  someAsyncProcess(function callback(result) {
    equal(result, expectedResult);

    // Close previous do_test_pending() call.
    do_test_finished();
  });
}

在子进程中进行测试

默认情况下,xpcshell 测试在父进程中运行。如果您希望在子进程中运行测试逻辑,则有几种方法可以做到这一点

  1. 创建一个常规的 test_foo.js 测试,然后编写一个包装器测试 test_foo_wrap.js 文件,该文件使用run_test_in_child()函数在子进程中运行整个脚本文件。这是一种简单的方法来安排测试运行两次,一次在 chrome 中,然后稍后(通过 _wrap.js 文件)在内容中运行。请参阅 /network/test/unit_ipc 以获取示例。run_test_in_child()函数接受一个回调,因此您可以使用不同的文件多次调用它,如果这很有用。

  2. 对于需要在单个测试运行期间在父进程和子进程中运行逻辑的测试,您可以使用文档记录不佳的sendCommand()函数,该函数接受要在子进程上执行的代码字符串,以及在完成后在父进程上运行的回调函数。您需要首先调用 do_load_child_test_harness() 以在子进程上设置合理的测试环境。sendCommand立即返回,因此您通常会希望与它一起使用do_test_pending/do_test_finished。注意:此测试方法使用不多,您的痛苦程度可能会很大。如果可能,请考虑选项 1。

有关更多信息,请参阅 testing/xpcshell/head.js 中run_test_in_child()do_load_child_test_harness()的文档。

特定于平台的测试

有时您可能需要一个测试来了解它运行在哪个平台上(以测试特定于平台的功能或允许不同的行为)。单元测试通常不会从 Makefile(与 Mochitests 不同)或预处理(因此不是 #ifdefs)中调用,因此使用这些方法进行平台检测并不容易。

运行时检测

某些测试可能只想在特定平台上执行某些部分。使用 AppConstants.sys.mjs 来确定平台,例如

let { AppConstants } =
  ChromeUtils.importESModule("resource://gre/modules/AppConstants.mjs");

let isMac = AppConstants.platform == "macosx";

有条件地运行测试

有两种不同的方法可以有条件地跳过测试,通过

通过 add_taskadd_test 函数添加条件

您可以对单个测试函数而不是整个文件使用条件语句。条件作为传递给 add_task()add_test() 的可选第一个参数提供。条件是一个包含名为 skip_if() 的函数的对象,该函数是一个 箭头函数,返回一个布尔值,如果应跳过测试,则为 **``true``**。

例如,您可以提供一个仅在 Mac OS X 上运行的测试,如下所示

let { AppConstants } =
  ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");

add_task({
  skip_if: () => AppConstants.platform != "mac"
}, async function some_test() {
  // Test code goes here
});

由于 AppConstants.platform != "mac" 仅在 Mac OS X 上测试时为 true,因此该测试将在所有其他平台上跳过。

注意

注意:箭头函数在这里很理想,因为如果您的条件比较常量,它将在测试运行之前就已经被评估过,这意味着您的输出将无法显示条件的具体内容。

在 xpcshell.toml 清单中添加条件

有时您可能希望添加条件来指定在某些配置中应跳过测试,或者在某些平台上已知测试会失败。您可以在 xpcshell 清单中通过在清单中测试文件条目下方添加注释来执行此操作,例如

[test_example.js]
skip-if = os == 'win'

此示例将跳过在 Windows 上运行 test_example.js

注意

注意:从 Gecko(Firefox 40/Thunderbird 40/SeaMonkey 2.37)开始,您可以对单个测试函数而不是整个文件使用条件语句。有关详细信息,请参阅上面的 通过 add_task 或 add_test 函数添加条件

目前您可以指定四个条件

skip-if

skip-if 告诉测试工具如果条件计算结果为 true,则跳过运行此测试。仅当测试在特定平台上没有意义或导致不必要的麻烦(例如使测试套件挂起很长时间)时,才应使用此选项。

run-if

run-if 告诉测试工具仅当条件计算结果为 true 时才运行此测试。它的功能与 skip-if 相反。

fail-if

fail-if 告诉测试工具如果条件为 true,则此测试预计会失败。如果您将其添加到测试中,请确保您针对此故障提交了一个错误,并在清单中的注释中包含错误编号,例如

[test_example.js]
# bug xxxxxx
fail-if = os == 'linux'

run-sequentially

run-sequentially 基本上告诉测试工具隔离运行相应的测试。对于不是“线程安全”的测试,这是必需的。您应该尽一切努力避免使用此选项,因为这会降低性能。但是,我们理解在某些情况下这是必要的,因此我们提供了此选项。如果您将其添加到测试中,请确保您指定了一个原因,甚至可能是一个错误编号,例如

[test_example.js]
run-sequentially = Has to launch Firefox binary, bug 123456.

清单条件表达式

有关条件表达式的语法的更详细说明以及哪些变量可用,请参阅此页面 </en/XPCshell_Test_Manifest_Expressions

仅运行特定测试

在处理特定功能或问题时,仅从整个测试套件中运行特定任务会很方便。为此目的使用 .only()

add_task(async function some_test() {
  // Some test.
});

add_task(async function some_interesting_test() {
// Only this test will be executed.
}).only();

挂起事件和关闭的问题

如果未显式触发,则在测试执行期间不会处理事件。这有时会导致关闭期间出现问题,当运行代码时,该代码预计先前创建的事件已经处理过。在这种情况下,测试结束时的此代码可以提供帮助

let thread = gThreadManager.currentThread;
while (thread.hasPendingEvents())
  thread.processNextEvent(true);

调试 xpcshell 测试

在 JavaScript 调试器下运行单元测试

通过 –jsdebugger

在发出 xpcshell-test 命令时,您可以指定标志,这将导致您的测试在运行之前停止,以便您可以附加 JavaScript 调试器

示例

$ ./mach xpcshell-test --jsdebugger browser/components/tests/unit/test_browserGlue_pingcentre.js
 0:00.50 INFO Running tests sequentially.
...
 0:00.68 INFO ""
 0:00.68 INFO "*******************************************************************"
 0:00.68 INFO "Waiting for the debugger to connect on port 6000"
 0:00.68 INFO ""
 0:00.68 INFO "To connect the debugger, open a Firefox instance, select 'Connect'"
 0:00.68 INFO "from the Developer menu and specify the port as 6000"
 0:00.68 INFO "*******************************************************************"
 0:00.68 INFO ""
 0:00.71 INFO "Still waiting for debugger to connect..."
...

在正在运行的 Firefox 实例的此阶段

  • 转到三条线菜单,然后选择 更多工具 -> 远程调试

  • 将打开一个新标签页。在“网络位置”框中,输入 localhost:6000 并选择 连接

  • 然后您应该获得指向``主进程``的链接,单击它,开发人员工具调试器窗口将打开。

  • 它将在测试开始时暂停,因此您可以添加断点或根据需要开始运行。

如果您收到以下消息

 0:00.62 ERROR Failed to initialize debugging: Error: resource://devtools appears to be inaccessible from the xpcshell environment.
This can usually be resolved by adding:
  firefox-appdir = browser
to the xpcshell.toml manifest.
It is possible for this to alter test behevior by triggering additional browser code to run, so check test behavior after making this change.

这通常是核心代码中的测试。您可以尝试将其添加到 xpcshell.toml 中,但是正如它所说,它可能会影响测试的运行方式并导致故障。通常,firefox-appdir 仅应在 xpcshell.toml 中保留用于浏览器/目录中的测试或仅限 Firefox 的测试。

在 C++ 调试器下运行单元测试

通过 --debugger -debugger-interactive

在发出 xpcshell-test 命令时,您可以指定标志,这将在指定的调试器中启动 xpcshell(在 错误 382682 中实现)。提供调试器的完整路径,或确保命名调试器位于您的系统 PATH 中。

示例

$ ./mach xpcshell-test --debugger gdb --debugger-interactive netwerk/test/unit/test_resumable_channel.js
# js>_execute_test();
...failure or success messages are printed to the console...
# js>quit();

在使用 VS 调试器的 Windows 上

$ ./mach xpcshell-test --debugger devenv --debugger-interactive netwerk/test/test_resumable_channel.js

或使用 WinDBG

$ ./mach xpcshell-test --debugger windbg --debugger-interactive netwerk/test/test_resumable_channel.js

或使用现代 WinDbg(截至 2020 年 4 月的 WinDbg 预览版)

$ ./mach xpcshell-test --debugger WinDbgX --debugger-interactive netwerk/test/test_resumable_channel.js

在子进程中调试 xpcshell 测试

要调试子进程(其中代码通常在项目中运行),请在您的环境(或命令行)中设置 MOZ_DEBUG_CHILD_PROCESS=1 并运行测试。您将看到子进程发出带有其进程 ID 的 printf,然后休眠。将调试器附加到子进程的 pid,当它唤醒时,您可以调试它

$ MOZ_DEBUG_CHILD_PROCESS=1 ./mach xpcshell-test test_simple_wrap.js
CHILDCHILDCHILDCHILD
  debug me @13476

调试父进程和子进程

使用 MOZ_DEBUG_CHILD_PROCESS=1 将调试器附加到每个进程。(至少对于 gdb 而言,这意味着运行 gdb 的单独副本,每个进程一个。)