如何测试铬扩展?

有什么好办法吗?我正在写一个扩展,作为一个内容脚本与网站交互,并使用本地存储保存数据。有没有什么工具、框架等可以用来测试这种行为?我知道有一些测试 javascript 的通用工具,但是这些工具是否足以测试一个扩展呢?单元测试是最重要的,但我也对其他类型的测试(例如集成测试)感兴趣。

51988 次浏览

是的,现有的框架非常有用。

最近,我将所有测试放在嵌入到应用程序中的“测试”页面上,但是除非物理键入,否则无法访问该页面。

例如,我将在 chrome-extension://asdasdasdasdad/unittests.html下访问一个页面中的所有测试

这些测试可以访问 localStorage等等。为了访问内容脚本,理论上你可以通过测试页面中嵌入的 IFRAME 进行测试,然而这些测试更多的是集成级别的测试,单元测试需要你从真实页面中抽象出来,这样你就不需要依赖它们了,同样的,你也可以访问 localStorage。

如果希望直接测试页面,可以编排扩展以打开新的选项卡(chrome.tab.create ({“ url”: “ somurl”}))。对于每个新的选项卡,您的内容脚本都应该运行,并且您可以使用您的测试框架来检查您的代码是否已经完成了它应该完成的任务。

至于框架,JsUnit或更新的 茉莉应该可以正常工作。

关于 Chrome 中已有的工具:

  1. 在 chrome 开发人员工具中,有一个关于本地存储的参考资料部分。

    开发工具 > 资源 > 本地存储

    查看本地存储的变化。

  2. 您可以使用 console.profile 来测试性能并观察运行时调用堆栈。

  3. For fileSystem 你可以使用这个 URL 来检查你的文件是否已经上传: 文件系统: chrome 扩展名:///临时/

如果您同时使用内容脚本和本地存储,而没有后台页面/脚本,也没有消息传递,那么本地存储将只能从该站点访问。 因此,要测试这些页面,您必须将测试脚本注入到这些选项卡中。

在几个铬扩展工作,我想出了 sinon-chrome项目,允许运行单元测试使用 mochanodejsphantomjs

基本上,它创建了所有 chrome.* API 的 sinon 模拟,您可以在其中放置任何预定义的 json 响应。

接下来,使用节点的 vm.runInNewContext作为后台页面加载脚本,使用 phantomjs作为呈现弹出/选项页面。

最后,您断言 chrome api 是用所需的参数调用的。

让我们举个例子:
假设我们有一个简单的 Chrome 扩展,它显示按钮徽章中打开的选项卡的数量。

背景页:

chrome.tabs.query({}, function(tabs) {
chrome.browserAction.setBadgeText({text: String(tabs.length)});
});

为了测试它,我们需要:

  1. Mock chrome.tabs.query返回预定义的响应,例如两个制表符。
  2. 将模拟的 chrome.* api 注入到某个环境中
  3. 在这个环境中运行我们的扩展代码
  4. 声明该按钮徽章等于“2”

代码片段如下:

const vm = require('vm');
const fs = require('fs');
const chrome = require('sinon-chrome');


// 1. mock `chrome.tabs.query` to return predefined response
chrome.tabs.query.yields([
{id: 1, title: 'Tab 1'},
{id: 2, title: 'Tab 2'}
]);


// 2. inject our mocked chrome.* api into some environment
const context = {
chrome: chrome
};


// 3. run our extension code in this environment
const code = fs.readFileSync('src/background.js');
vm.runInNewContext(code, context);


// 4. assert that button badge equals to '2'
sinon.assert.calledOnce(chrome.browserAction.setBadgeText);
sinon.assert.calledWithMatch(chrome.browserAction.setBadgeText, {
text: "2"
});

现在我们可以将它包装到摩卡的 describe..it函数中,然后从终端运行:

$ mocha


background page
✓ should display opened tabs count in button badge


1 passing (98ms)

你可以找到完整的例子 给你

此外,sinon-chrome 允许触发任何具有预定义响应的 chrome 事件,例如。

chrome.tab.onCreated.trigger({url: 'http://google.com'});

虽然 sinon.js看起来工作得很好,你也可以使用简单的 Jasmine 和模仿你需要的 Chrome 回调。例如:

嘲笑

chrome = {
runtime: {
onMessage : {
addListener : function() {}
}
}
}

测试

describe("JSGuardian", function() {


describe("BlockCache", function() {


beforeEach(function() {
this.blockCache = new BlockCache();
});


it("should recognize added urls", function() {
this.blockCache.add("http://some.url");
expect(this.blockCache.allow("http://some.url")).toBe(false);
});
} // ... etc

只需修改默认的 SpecRunner.html来运行代码。

我发现我可以使用 硒网络驱动程序来启动带有预安装扩展的新浏览器实例,使用 Pyautogui来单击——因为 Selenium 不能驱动扩展的“视图”。在点击之后,你可以制作屏幕截图,并将它们与“预期的”截图进行比较,期望有95% 的相似性(因为在不同的浏览器上,可以接受几个像素的标记移动)。

为了证实之前的几个答案,Jasmine 似乎在 Chrome 扩展中工作得很好,我使用的是3.4.0版本。

您可以使用 茉莉间谍轻松地为各种 API 创建测试替身。没必要自己动手。例如:

describe("Test suite", function() {


it("Test case", function() {


// Set up spies and fake data.
spyOn(chrome.browserAction, "setPopup");
spyOn(chrome.identity, "removeCachedAuthToken");
fakeToken = "faketoken-faketoken-faketoken";
fakeWindow = jasmine.createSpyObj("window", ["close"]);


// Call the function under test.
logout(fakeWindow, fakeToken);


// Perform assertions.
expect(chrome.browserAction.setPopup).toHaveBeenCalledWith({popup: ""});
expect(chrome.identity.removeCachedAuthToken).toHaveBeenCalledWith({token: fakeToken});
expect(fakeWindow.close.calls.count()).toEqual(1);


});


});

更多细节,如果有帮助的话:

正如在另一个答案中提到的,我创建了一个 HTML 页面,作为运行测试的浏览器扩展的一部分。HTML 页面包括 Jasmine 库,以及我的扩展的 JavaScript 代码,还有我的测试套件。测试将自动运行,并为您格式化结果。不需要构建测试运行程序或结果格式化程序。只要按照 安装说明书,并使用那里记录的 HTML 来创建您的测试运行程序页面,并在页面中包含您的测试套件。

我不认为您可以从另一个主机动态地获取 Jasmine 框架,所以我只是在我的扩展中包含了 Jasmine 发行版。当然,在为生产构建扩展时,我会省略它和我的测试用例。

我还没有研究如何在命令行中执行测试。这对于自动化部署工具来说非常方便。

要测试端到端,可以使用 puppeteer。 下面是我为我的扩展编写的代码片段,用来检查加载的扩展 title,并验证扩展是否在隐身模式下启用。

const path = require("path");
const puppeteer = require("puppeteer");
const assert = require("assert");
const Constants = require("../contants");
const Utils = require("./util");


const extensionID = Constants.EXTENSION_ID;
const extensionPath = path.join(__dirname, "../dist");
const extensionOptionHtml = "option.html";
const extPage = `chrome-extension://${extensionID}/${extensionOptionHtml}`;
let extensionPage = null;
let browser = null;


async function boot() {
browser = await puppeteer.launch({
// slowMo: 250,
headless: false, // extension are allowed only in head-full mode
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
"--no-sandbox",
"--disable-setuid-sandbox"
]
});


extensionPage = await browser.newPage();
await extensionPage.goto(extPage);
}


describe("Extension UI Testing", function() {
this.timeout(20000); // default is 2 seconds and that may not be enough to boot browsers and pages.
before(async function() {
await boot();
});


describe("option page home", async function() {
it("check title", async function() {
const h1 = "Allow extension in Incognito Mode";
const extH1 = await extensionPage.evaluate(() =>
document.querySelector("h1").textContent.trim()
);
assert.equal(extH1, h1);
});
it("show option ui after enabling extension in incognito", async () => {
await extensionPage.goto(`chrome://extensions/?id=${extensionID}`);
extensionPage.evaluate(() =>
document
.querySelector("body > extensions-manager")
.shadowRoot.querySelector("#viewManager > extensions-detail-view")
.shadowRoot.querySelector("#allow-incognito")
.shadowRoot.querySelector("#crToggle")
.click()
);
await Utils.sleep(2000);
await extensionPage.goto(
`chrome-extension://${extensionID}/${extensionOptionHtml}`
);
const h3 = "Mark Incognito";
const headingID = `#${Constants.OPTION_SCRIPT_HOST_ID} > div > div > header > div > h6`;
await extensionPage.waitFor(headingID);
console.log({ headingID });
const extH3 = await extensionPage.evaluate(headingID => {
return document.querySelector(headingID).textContent.trim();
}, headingID);
console.log({ extH3 });
assert.equal(extH3, h3);
});
});


after(async function() {
await browser.close();
});
});