Skip to content

Commit

Permalink
Add a helper function for writing parameterized JS tests
Browse files Browse the repository at this point in the history
Mocha lacks built-in support [1] for writing parameterized tests and the
suggested solution [2] involves a bunch of boilerplate* which has IMO
resulted in different styles of parameterized tests in our codebase and
not having parameterized tests when they would be useful to attain more
complete coverage.

This adds a helper inspired by [3] for writing parameterized tests and
switches several existing places in our code to use it.

* Though less with ES2015 syntax.

[1] mochajs/mocha#1454
[2] https://mochajs.org/#dynamically-generating-tests
[3] https://github.com/lawrencec/Unroll
  • Loading branch information
robertknight committed Apr 18, 2016
1 parent a26c807 commit 28c35fe
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 58 deletions.
37 changes: 18 additions & 19 deletions h/browser/chrome/test/uri-info-test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use strict';

var toResult = require('../../../static/scripts/test/promise-util').toResult;
var unroll = require('../../../static/scripts/test/util').unroll;

var uriInfo = require('../lib/uri-info');
var settings = require('./settings.json');

var toResult = require('../../../static/scripts/test/promise-util').toResult;

describe('UriInfo.query', function () {
var badgeURL = settings.apiUrl + '/badge';

Expand Down Expand Up @@ -45,25 +46,23 @@ describe('UriInfo.query', function () {
});
});

var INVALID_RESPONSES = [
[200, {}, 'this is not valid json'],
[200, {}, '{"total": "not a valid number"}'],
[200, {}, '{"rows": []}'],
var INVALID_RESPONSE_FIXTURES = [
{status: 200, headers: {}, body: 'this is not valid json'},
{status: 200, headers: {}, body: '{"total": "not a valid number"}'},
{status: 200, headers: {}, body: '{"rows": []}'},
];

INVALID_RESPONSES.forEach(function (response) {
it('returns an error if the server\'s JSON is invalid', function () {
fetch.returns(
Promise.resolve(
new window.Response(
response[2],
{status: response[0], headers: response[1]}
)
unroll('returns an error if the server\'s JSON is invalid', function (response) {
fetch.returns(
Promise.resolve(
new window.Response(
response.body,
{status: response.status, headers: response.headers}
)
);
return toResult(uriInfo.query('tabUrl')).then(function (result) {
assert.ok(result.error);
});
)
);
return toResult(uriInfo.query('tabUrl')).then(function (result) {
assert.ok(result.error);
});
});
}, INVALID_RESPONSE_FIXTURES);
});
19 changes: 9 additions & 10 deletions h/static/scripts/directive/test/search-status-bar-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
var angular = require('angular');

var util = require('./util');
var unroll = require('../../test/util').unroll;

describe('searchStatusBar', function () {
before(function () {
Expand All @@ -25,21 +26,19 @@ describe('searchStatusBar', function () {
});

context('when there is a selection', function () {
var cases = [
var FIXTURES = [
{count: 0, message: 'Show all annotations'},
{count: 1, message: 'Show all annotations'},
{count: 10, message: 'Show all 10 annotations'},
];

cases.forEach(function (testCase) {
it('should display the "Show all annotations" message', function () {
var elem = util.createDirective(document, 'searchStatusBar', {
selectionCount: 1,
totalCount: testCase.count
});
var clearBtn = elem[0].querySelector('button');
assert.include(clearBtn.textContent, testCase.message);
unroll('should display the "Show all annotations" message when there are #count annotations', function (testCase) {
var elem = util.createDirective(document, 'searchStatusBar', {
selectionCount: 1,
totalCount: testCase.count
});
});
var clearBtn = elem[0].querySelector('button');
assert.include(clearBtn.textContent, testCase.message);
}, FIXTURES);
});
});
52 changes: 25 additions & 27 deletions h/static/scripts/test/markdown-commands-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var commands = require('../markdown-commands');
var unroll = require('./util').unroll;

/**
* Convert a string containing '<sel>' and '</sel>' markers
Expand Down Expand Up @@ -72,33 +73,30 @@ describe('markdown commands', function () {
});

describe('block formatting', function () {
var CASES = {
'adds formatting to blocks': {
input: 'one\n<sel>two\nthree</sel>\nfour',
output: 'one\n> <sel>two\n> three</sel>\nfour',
},
'removes formatting from blocks': {
input: 'one \n<sel>> two\n> three</sel>\nfour',
output: 'one \n<sel>two\nthree</sel>\nfour',
},
'preserves the selection': {
input: 'one <sel>two\nthree </sel>four',
output: '> one <sel>two\n> three </sel>four',
},
'inserts the block prefix before an empty selection': {
input: '<sel></sel>',
output: '> <sel></sel>',
}
};

Object.keys(CASES).forEach(function (case_) {
it(case_, function () {
var output = commands.toggleBlockStyle(
parseState(CASES[case_].input), '> '
);
assert.equal(formatState(output), CASES[case_].output);
});
});
var FIXTURES = [{
tag: 'adds formatting to blocks',
input: 'one\n<sel>two\nthree</sel>\nfour',
output: 'one\n> <sel>two\n> three</sel>\nfour',
},{
tag: 'removes formatting from blocks',
input: 'one \n<sel>> two\n> three</sel>\nfour',
output: 'one \n<sel>two\nthree</sel>\nfour',
},{
tag: 'preserves the selection',
input: 'one <sel>two\nthree </sel>four',
output: '> one <sel>two\n> three </sel>four',
},{
tag: 'inserts the block prefix before an empty selection',
input: '<sel></sel>',
output: '> <sel></sel>',
}];

unroll('#tag', function (fixture) {
var output = commands.toggleBlockStyle(
parseState(fixture.input), '> '
);
assert.equal(formatState(output), fixture.output);
}, FIXTURES);
});

describe('link formatting', function () {
Expand Down
4 changes: 2 additions & 2 deletions h/static/scripts/test/media-embedder-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('media-embedder', function () {
var urls = [
'https://youtu.be/QCkm0lL-6lc',
'https://youtu.be/QCkm0lL-6lc/',
]
];
urls.forEach(function (url) {
var element = domElement('<a href="' + url + '">' + url + '</a>');

Expand All @@ -55,7 +55,7 @@ describe('media-embedder', function () {
'https://vimeo.com/149000090/#fragment',
'https://vimeo.com/149000090?foo=bar&a=b',
'https://vimeo.com/149000090/?foo=bar&a=b',
]
];
urls.forEach(function (url) {
var element = domElement('<a href="' + url + '">' + url + '</a>');

Expand Down
55 changes: 55 additions & 0 deletions h/static/scripts/test/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,61 @@ function noCallThru(stubs) {
return Object.assign(stubs, {'@noCallThru':true});
}

/**
* Helper for writing parameterized tests.
*
* This is a wrapper around the `it()` function for creating a Mocha test case
* which takes an array of fixture objects and calls it() once for each fixture,
* passing in the fixture object as an argument to the test function.
*
* Usage:
* unroll('should return #output with #input', function (fixture) {
* assert.equal(functionUnderTest(fixture.input), fixture.output);
* },[
* {input: 'foo', output: 'bar'}
* ]);
*
* Based on https://github.com/lawrencec/Unroll with the following changes:
*
* 1. Support for test functions that return promises
* 2. Mocha's `it()` is the only supported test function
* 3. Fixtures are objects rather than arrays
*
* @param {string} description - Description with optional '#key' placeholders
* which are replaced by the values of the corresponding key from each
* fixture object.
* @param {Function} testFn - Test function which as arguments either a fixture
* object from the `fixtures` array, or the `done` callback to invoke
* when the async test completes, followed by the object from the
* `fixtures` array.
* @param {Array<T>} fixtures - Array of fixture objects.
*/
function unroll(description, testFn, fixtures) {
fixtures.forEach(function (fixture) {
var caseDescription = Object.keys(fixture).reduce(function (desc, key) {
return desc.replace('#' + key, String(fixture[key]));
}, description);
it(caseDescription, function (done) {
if (testFn.length === 1) {
// Test case does not accept a 'done' callback argument, so we either
// call done() immediately if it returns a non-Promiselike object
// or when the Promise resolves otherwise
var result = testFn(fixture);
if (typeof result === 'object' && result.then) {
result.then(function () { done(); }, done);
} else {
done();
}
} else {
// Test case accepts a 'done' callback argument and takes responsibility
// for calling it when the test completes.
testFn(done, fixture);
}
});
});
}

module.exports = {
noCallThru: noCallThru,
unroll: unroll,
};

0 comments on commit 28c35fe

Please sign in to comment.