如何测试Chrome扩展?

187

有没有好的方法可以做到这一点?我正在编写一个扩展程序,作为内容脚本与网站进行交互,并使用localstorage保存数据。是否有任何工具、框架等可以用来测试此行为?我知道有一些通用的测试JavaScript的工具,但它们足以测试扩展程序吗?单元测试是最重要的,但我也对其他类型的测试(如集成测试)感兴趣。


8
我刚刚撰写了一篇通用答案,讨论了跨所有浏览器(而不仅仅是Chrome)进行浏览器扩展的单元测试和集成测试。请参阅“测试浏览器扩展”的答案(https://dev59.com/wWUq5IYBdhLWcg3wF8hQ#17370531)。 - Rob W
7个回答

125

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

最近,我把所有的测试放在一个“测试”页面上,这个页面被嵌入到应用程序中,但除非物理输入,否则无法访问。

例如,我会把所有的测试放在一个页面上,可以通过 chrome-extension://asdasdasdasdad/unittests.html 访问。

测试将可以访问 localStorage 等。如果您想访问内容脚本,则理论上可以通过测试页面中的嵌入IFRAME来测试,但这些更像是集成级别的测试,单元测试需要将其从真实页面中抽象出来,以便您不要依赖它们,同样地,请考虑局限于localStorage的访问。

如果您想直接测试页面,可以编排您的扩展程序以打开新标签页(chrome.tab.create({"url" :" someurl "})。对于每个新标签页,您的内容脚本都应该运行,并且您可以使用测试框架来检查您的代码是否做了应该做的事情。

至于框架,JsUnit 或更新的 Jasmine 应该能够很好地工作。


1
你说得对,测试真实页面不属于单元测试的范畴。我应该把我的问题提得更加广泛。但是这仍然是我想要测试的东西,特别是网站的 HTML 结构随时可能会发生变化。我已经修改了问题。 - swampsjohn
2
我仍然会在您的单元测试页面中通过IFrames进行测试。如果您启用脚本在iFrame中运行,内容脚本仍然应该触发。 - Kinlan
3
代理示例扩展程序有一些测试,只是模拟了所需的Chrome API的各个部分:http://code.google.com/chrome/extensions/samples.html#chrome.proxy。此外,我们的同事Boris在测试他的“模型”层时使用了QUnit:https://github.com/borismus/Question-Monitor-for-Stack-Exchange/tree/master/tests。 - Paul Irish

71

我开发了几个Chrome扩展程序,其中包括sinon-chrome项目,它允许使用mochanodejsphantomjs运行单元测试。

基本上,它创建了所有chrome.* API的sinon mocks,您可以放置任何预定义的JSON响应。

接下来,您可以使用node的vm.runInNewContext加载后台页面,使用phantomjs加载弹出窗口/选项页面。

最后,您可以断言Chrome API是否被调用并传入所需的参数。

让我们来看一个例子:
假设我们有一个简单的Chrome扩展程序,它在按钮徽章中显示打开标签页的数量。

后台页面:

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

为测试它,我们需要:

  1. 模拟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"
});

现在我们可以将其包装在Mocha的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'});

1
示例的链接似乎已经失效了 - 请问您能否更新一下? - Raisen
1
示例的链接已更新。另外,sinon-chrome现在已经移至https://github.com/acvetkov,很快会有新的示例。 - vitalets
注意:该项目似乎已经停滞不前:最后一次提交是在2019年11月26日,问题仍未得到回复。 - miwe

5

虽然sinon.js非常好用,但你也可以只使用普通的Jasmine框架来模拟需要的Chrome回调。示例代码如下:

Mock

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文件即可运行您的代码。


3

关于Chrome中已存在的工具:

  1. 在Chrome的开发者工具中,有一个用于本地存储的资源部分。

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

    可以在那里查看localstorage的更改。

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

  3. 对于fileSystem,您可以使用以下URL检查您的文件是否上传: filesystem:chrome-extension:///temporary/

如果您同时使用内容脚本和本地存储,而没有背景页/脚本和没有消息传递,则只有该站点才能访问local-storage。因此,要测试这些页面,您必须将测试脚本注入到这些选项卡中。


1
对我来说没有起作用,但它确实让我的JavaScript进展更快了。为此点赞。 - mobibob
对于文件系统,您可以使用:filesystem:chrome-extension://<yourextension-id>/temporary/ - Nafis Ahmad

2
我发现我可以使用Selenium web driver来启动预装扩展的全新浏览器实例,pyautogui用于点击 - 因为Selenium无法驱动扩展的“视图”。点击后,您可以进行屏幕截图并将其与“预期”的屏幕截图进行比较,预计相似度达到95%(因为在不同的浏览器上,对少量像素的标记移动是可接受的)。

2

要测试端到端,您可以使用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();
  });
});


1

确认之前的回答,Jasmine 与 Chrome 扩展程序兼容良好。我正在使用版本 3.4.0。

您可以使用 Jasmine spies轻松创建各种 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版本。当然,在生产环境中构建我的扩展时,我会省略它以及我的测试用例。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接