自动化测试:编写测试

添加新的浏览器 chrome 测试

创建新的测试文件几乎总是比向现有测试文件添加新的测试用例要好。

这可以防止测试文件增长到运行时间过长而导致超时。测试系统有时可能会承受很大的压力,并且运行速度比你通常的本地环境慢得多。

它还有助于使测试更易于维护:使用许多小文件,更容易跟踪问题,而不是在一个巨大的文件中。

创建新文件

首先,你需要创建一个文件。此文件应放在其测试的代码旁边,位于 tests 目录中。例如,一个检查器测试将位于 devtools/inspector/test/ 中。

命名新文件

命名你的文件非常重要,可以帮助其他人了解它应该测试什么。话虽如此,名称也不应该太长。

一个好的命名约定是 browser_<panel>_<short-description>[_N].js

其中

  • <panel>debuggermarkupviewinspectorruleview 等之一。

  • <short-description> 应为大约 3 到 4 个单词,用连字符 (-) 分隔。

  • 并且如果有多个文件测试同一件事,则可以选择在末尾添加一个数字。

例如:browser_ruleview_completion-existing-property_01.js

请注意,并非所有现有测试的命名都一致。因此,我们尝试遵循的规则是**与同一测试文件夹中其他测试的命名方式保持一致**。

测试的基本结构

/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";

// A detailed description of what the test is supposed to test

const TEST_URL = TEST_URL_ROOT + "doc_some_test_page.html";

add_task(async function() {
	await addTab(TEST_URL_ROOT);
	let {toolbox, inspector, view} = await openRuleView();
	await selectNode("#testNode", inspector);
	await checkSomethingFirst(view);
	await checkSomethingElse(view);
});

async function checkSomethingFirst(view) {
/* ... do something ... this function can await */
}

async function checkSomethingElse(view) {
/* ... do something ... this function can await */
}

引用新文件

为了运行你的测试,需要在同一目录中找到的 browser.ini 文件中引用它。例如:browser/devtools/debugger/test/browser.ini

添加一行包含你的文件名(用方括号括起来),并确保文件列表**始终按字母顺序排序**(有些列表可能非常长,因此字母顺序有助于查找和推理)。

例如,如果你要添加上一节中的测试,则需要将其添加到 browser.ini

[browser_ruleview_completion-existing-property_01.js]

添加支持文件

有时你的测试可能需要在一个选项卡中打开 HTML 文件,并且它可能还需要加载 CSS 或 JavaScript。为了使这能够正常工作,你需要…

  1. 将这些文件放在同一目录中,并且

  2. browser.ini 文件中引用它们。

支持文件有一个命名约定:doc_<support-some-test>.html

但是同样,名称通常不遵循此约定,因此请尝试遵循当前在同一测试目录中的其他支持文件的样式。

要引用你的新支持文件,请在 browser.inisupport-files 部分中添加其文件名,同时也要确保此部分按字母顺序排列。

支持文件可以通过在测试运行时启动的本地服务器访问。此服务器可在 http://example.com/browser/ 访问。有关更多信息,请参阅下面的 head.js section 部分。

利用 head.js 中的辅助函数

head.js 是一个特殊的支持文件,在测试运行的范围内加载,并在测试开始之前加载。它包含对大多数测试都很有用的全局辅助函数。阅读测试目录中的 head.js 文件以查看有哪些函数,从而避免重复代码。

DevTools 中的每个面板都有自己的测试目录,以及自己的 head.js,因此你将在每个面板的 head.js 文件中找到不同的内容。

例如,markupviewstyleinspector 测试文件夹中的 head.js 文件包含以下有用的函数和常量

  • 支持文件的基准 URL:TEST_URL_ROOT。这避免了在所有测试中都重复 http://example.com/browser/browser/devtools/styleinspector/ URL 片段,

  • waitForExplicitFinish()head.js 中调用一次,并且仅此一次。所有测试都是异步的,因此无需在每个测试中再次调用它,

  • auto-cleanup:工具箱会自动关闭,所有选项卡也会关闭,

  • tab addTab(url)

  • {toolbox, inspector} openInspector()

  • {toolbox, inspector, view} openRuleView()

  • selectNode(selectorOrNode, inspector)

  • node getNode(selectorOrNode)

共享的 head.js 文件

已引入 shared-head.js 文件以避免在各种 head.js 文件中重复代码。

重要的是要知道你的测试目录中的 shared.js 是否已经导入 shared-head.js(查找 Services.scriptloader.loadSubScript 调用),因为 shared-head.js 中的常用辅助函数可能对你的测试有用。

如果你计划处理大量新测试,如果你的 head.js 中尚未包含 shared-head.js,那么实际上在你的 head.js 中导入 shared-head.js 可能值得花费时间。

Electrolysis

E10S 是 Firefox 多进程的代号,这对我们来说意味着测试运行的进程与测试内容页面运行的进程不同。

你可以从这篇博文Electrolysis wiki 页面以及有关测试和 E10s的页面了解更多有关 E10S 的信息。

E10S 对测试的一个直接影响是,你无法像在没有 E10S 的情况下那样检索和操作内容页面中的对象。

因此,在创建新测试时,如果此测试需要以任何方式访问内容页面,则可以使用消息管理器或 JSActors与加载在内容进程中的脚本进行通信,以代替直接访问页面中的对象来为你执行操作。

你可以为此使用辅助函数 ContentTask.spawn()。请参阅此使用该辅助函数的 DevTools 测试列表以获取示例

请注意,很多测试只需要访问 DevTools UI,根本不需要与内容进程交互。由于 UI 与测试位于同一进程中,因此你无需使用消息管理器来访问它。

异步测试

大多数浏览器 chrome DevTools 测试都是异步的。它们是异步的原因之一是,代码需要为工具中的各种用户交互注册事件处理程序,然后模拟这些交互。另一个原因是,大多数 DevTools 操作是通过调试器协议异步完成的。

以下是一些关于异步测试需要注意的事项

  • head.js 已经调用了 waitForExplicitFinish(),因此你的新测试不需要再调用它。

  • 使用带有异步函数的 add_task 意味着你可以等待返回 Promise 的函数的调用。这也意味着你的主测试函数可以通过在调用异步函数之前添加 await 来编写成几乎看起来像同步代码的形式。例如

for (let test of testData) {
	await testCompletion(test, editor, view);
}

每次调用 testCompletion 都是异步的,但代码不需要依赖嵌套回调并维护索引,可以使用标准的 for 循环。

编写简洁、易维护的测试代码

测试代码与功能代码本身一样重要,它有助于避免回归,但它还有助于理解否则难以理解的代码的复杂部分。

由于我们发现自己在很大一部分时间里都在使用测试代码,因此我们应该花费时间和精力来使这段时间变得愉快。

日志和注释

阅读测试输出日志并不有趣,需要花费时间,但在某些时候是必要的。确保你的测试通过使用以下方法生成足够的日志

info("doing something now")

如果测试失败,知道测试在哪些行附近失败非常有帮助。

一个好的经验法则是,如果你要在你测试中添加 JS 行注释来解释下面的代码将要测试什么,请在 info() 中编写相同的注释。

还在文件顶部添加描述以帮助理解此测试的用途。文件名通常不够长以传达你需要了解的关于测试的所有信息。理解测试通常可以让你了解功能本身。

不是真正的注释,但不要忘记“use strict”;

回调和 Promise

避免多个嵌套回调或链式 Promise。它们使代码难以阅读。改用 async/await。

清理你的工作

不要在测试文件中公开全局变量,它们最终可能会导致难以跟踪的错误。 head.js 中的大多数函数都返回 DevTools 面板的有用实例,你可以将这些实例作为参数传递给你的子函数,无需将它们存储在全局作用域中。这避免了在最后不得不记住清空它们。

如果你的测试需要切换用户偏好设置,请确保在测试结束时重置这些偏好设置。但是不要在测试函数的末尾重置它们,因为如果你的测试失败,偏好设置将永远不会被重置。改用 registerCleanupFunction 辅助函数。

最好在 head.js 中进行重置。

编写小型、易维护的代码

将你的主要测试函数拆分成更小的测试函数,并使用自解释的名称。

确保你的测试文件很小。如果你正在开发一个新功能,你可以在每次添加新功能时创建一个新的测试,例如向UI添加一个新的按钮。这有助于进行小的、增量的测试,并且还可以帮助在编码时编写测试。

如果你的测试只是一系列重复调用相同函数的序列,那么最好将测试步骤描述在一个数组中,并只使用一个函数来运行数组中的每个项目。请参见以下示例

const TESTS = [
	{desc: "add a class", cssSelector: "#id1", makeChanges: async function() {...}},
	{desc: "change href", cssSelector: "a.the-link", makeChanges: async function() {...}},
	...
];

add_task(async function() {
	await addTab("...");
	let {toolbox, inspector} = await openInspector();
	for (let step of TESTS) {
	  info("Testing step: " + step.desc);
	  await selectNode(step.cssSelector, inspector);
	  await step.makeChanges();
	}
});

如本代码示例所示,你可以在TESTS数组中添加任意数量的测试用例,而实际的测试代码将保持非常短,并且易于理解和维护(请注意,当循环遍历测试数组时,始终建议添加一个“desc”属性,该属性将在info()日志输出中使用)。

避免异常

即使它们没有导致测试失败,异常也是不好的,因为它们会污染日志并使其更难以阅读。它们也不好,因为当你的测试作为测试套件的一部分运行时,如果另一个不相关的测试失败,那么异常可能会向修复不相关测试的人员提供错误的信息。

在你的测试在本地运行后,只需确保它没有输出异常即可,方法是滚动浏览日志。

通常,非阻塞异常可能是由挂起的协议请求引起的,当工具在测试结束时关闭时,这些请求尚未得到响应。确保你注册到正确的事件,并留出时间让工具更新自身,然后再继续。

避免测试超时

当测试失败时,最好让它们立即失败并以异常结束,这将有助于修复它,而不是让它们挂起直到它们达到超时并被终止。

添加新的辅助函数

在某些情况下,你可能希望从测试中提取一些公共代码以在另一个测试中使用它。

  • 如果这是所有测试都可能使用的非常常见的代码,则将其添加到devtools/client/shared/test/shared-head.js中。

  • 如果这是特定于给定工具的公共代码,则将其添加到相应的head.js文件中。

  • 如果它不够常见以至于无法放在head.js中,那么创建一个辅助文件以避免重复也是一个好主意。以下是创建辅助文件的方法

  • 在你的测试目录中创建一个新文件。命名约定应为helper_<description_of_the_helper>.js

  • 将其添加到browser.ini support-files部分,确保按字母顺序排序

  • 在测试中加载辅助文件

  • browser/devtools/markupview/test/head.js有一个方便的loadHelperScript(fileName)函数,你可以使用它。

  • 该文件将在测试全局作用域中加载,因此它定义的任何全局函数或变量都将可用(就像head.js一样)。

  • 使用特殊的ESLint注释/* import-globals-from helper_file.js */来防止ESLint对未定义变量的错误。

在所有情况下,新的辅助函数都应使用jsdoc注释块进行适当的注释。