Skip to content

Commit

Permalink
Unit tests for addon/lib/metrics.js
Browse files Browse the repository at this point in the history
- Use the downloaded Firefox for both integration and add-on unit tests.

- Switch from Ci.import() to require() for some JS modules in
  addon/lib/metrics.js, making them replaceable with mock APIs

- New tests in addon/test/test-metrics.js

- Include add-on testing in bin/circleci/build-addon.sh

- Utilities for mocking out API functions and monitoring calls

- Custom module loader to substitute mock modules on require() calls in
  the test subject module

Issue mozilla#1220
  • Loading branch information
lmorchard committed Aug 17, 2016
1 parent cbeb50f commit 43eb39d
Show file tree
Hide file tree
Showing 9 changed files with 479 additions and 8 deletions.
10 changes: 10 additions & 0 deletions addon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ A relatively easy path for working on this addon involves the following steps:

For UI hacking you can run `npm run watch-ui` to easily debug `lib/templates.js` and `data/panel.css`

## tests

Unit tests for the add-on are run via `jpm` as an `npm` script:

```
npm test -- --binary=/Applications/Nightly.app/Contents/MacOS/firefox-bin
```

Look in the `test` directory for examples of tests.

## running once for testing

* Install [Firefox Beta][fxbeta]
Expand Down
9 changes: 2 additions & 7 deletions addon/lib/metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@
* http://mozilla.org/MPL/2.0/.
*/

/* global TelemetryController, Services */

const {Cu} = require('chrome');

Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/TelemetryController.jsm');

const { AddonManager } = require('resource://gre/modules/AddonManager.jsm');
const { Services } = require('resource://gre/modules/Services.jsm');
const { TelemetryController } = require('resource://gre/modules/TelemetryController.jsm');
const Events = require('sdk/system/events');
const PrefsService = require('sdk/preferences/service');
const self = require('sdk/self');
Expand Down
1 change: 1 addition & 0 deletions addon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"updateURL": "https://testpilot.firefox.com/static/addon/update.rdf",
"updateLink": "https://testpilot.firefox.com/static/addon/addon.xpi",
"scripts": {
"test": "./node_modules/.bin/jpm test --verbose",
"start": "npm run watch",
"once": "jpm run -b beta --prefs dev-prefs.json",
"watch": "jpm watchpost --post-url http://127.0.0.1:8888",
Expand Down
103 changes: 103 additions & 0 deletions addon/test/lib/mock-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const { Loader, Require, unload,
override, descriptor } = require('toolkit/loader');
const { ensure } = require('sdk/system/unload');

let debug = false;

exports.setDebug = (flag) => debug = flag;

exports.callback = function(name) {
let callState = [];
let implementCb = null;

const fn = function() {
const args = Array.prototype.slice.call(arguments);
args.timestamp = Date.now();
if (debug) {
console.log(name, args); // eslint-disable-line no-console
}
callState.push(args);
if (implementCb) {
return implementCb.apply(fn, args);
}
};

fn.reset = () => {
callState = [];
implementCb = null;
};
fn.calls = () => callState;
fn.implement = cb => implementCb = cb;

return fn;
};

exports.callbacks = function(spec) {
const out = {};
Object.keys(spec).forEach(prefix => {
out[prefix] = {};
spec[prefix].forEach(name => {
out[prefix][name] = exports.callback(prefix + '.' + name);
});
});
return out;
};

exports.resetCallbacks = function(callbacks) {
Object.keys(callbacks).forEach(prefix => {
const items = callbacks[prefix];
Object.keys(items).forEach(name => items[name].reset());
});
};

// Build a custom module loader that substitutes mock modules for the test
// subject's require() calls.
exports.loader = function(testModule, subjectPath, modules) {
const loaderOptions = require('@loader/options');

// Get a handle on the original resolver created by default in Loader.
const realResolve = loaderOptions.isNative ?
(id, requirer) => Loader.nodeResolve(id, requirer, {rootURI: loaderOptions.rootURI}) :
Loader.resolve;

// Custom resolver that substitutes mock modules, but only when the test
// subject module is asking for them.
const mockResolve = (id, requirer) => {
let resolvedPath = realResolve(id, requirer);
// Only trigger mock module substitution for the test subject
if (requirer === subjectPath) {
// If the real resolver came up with no mapping, use the original ID
if (!resolvedPath) { resolvedPath = id; }
// If the path is in our set of mock substitutions, use the prefix.
if (resolvedPath in modules) {
resolvedPath = 'mock:' + resolvedPath;
// Announce the substitution if debug is on.
if (debug) {
console.log('Substituting mock', requirer, '<=', id); // eslint-disable-line no-console
}
}
}
return resolvedPath;
};

// Prefix all the supplied mock modules for conditional injection
const mockModules = {};
Object.keys(modules).forEach(path => {
mockModules['mock:' + path] = modules[path];
});

// Build and return the mock module loader
const loader = Loader(override( // eslint-disable-line new-cap
loaderOptions,
{modules: mockModules, resolve: mockResolve}
));

const mockLoader = Object.create(loader, descriptor({
require: Require(loader, testModule), // eslint-disable-line new-cap
unload: reason => unload(loader, reason)
}));

ensure(mockLoader);

return mockLoader;
};
Loading

0 comments on commit 43eb39d

Please sign in to comment.