I'm trying to write some unit tests for an Apps Script add-on designed for Google Docs. A few of the functions I'd like to have unit tests for call PropertiesService.getDocumentProperties()
. A simple example function in my add-on:
function baseFontSize() {
var baseFontSize = JSON.parse(
PropertiesService.getDocumentProperties().getProperty('baseFontSize'));
if (baseFontSize === null) {
baseFontSize = JSON.parse(
PropertiesService.getUserProperties().getProperty('baseFontSize'));
if (baseFontSize === null) {
PropertiesService.getUserProperties().setProperty('baseFontSize', '11');
baseFontSize = 11;
}
PropertiesService.getDocumentProperties()
.setProperty('baseFontSize', JSON.stringify(baseFontSize));
}
return baseFontSize;
}
I'm writing my tests with the QUnit for Google Apps Script library:
function doGet(e) {
QUnit.urlParams(e.parameter);
QUnit.config({title: 'My test suite'});
QUnit.load(testSuite);
return QUnit.getHtml();
}
QUnit.helpers(this);
function testSuite() {
// some module() and test() calls deleted for brevity...
test('baseFontSize', function() {
// PropertiesService.getDocumentProperties() === null
// how to test baseFontSize()?
});
// more module() and test() calls...
}
Since the test suite is not running within a document, there are no document properties. It seems that the only way to test my function would be to mock the getDocumentProperties
function. Of course, the only Apps Script mock/stub libraries I can find are either meant to test within a Node.js environment, or are not sufficiently complete for my needs, which means I would have to roll my own.
While I still hope to find a more elegant solution, until that occurs I have thrown together a simple stub framework based on the SinonJS API, and I'm injecting the stub into my function to be tested (since I can't actually stub PropertiesService
, as it's frozen).
Roughly, my solution (my project already includes Underscore, so I take advantage of it where I can):
function stub(object, property, impl) {
const original = object[property];
if (_.isFunction(impl)) object[property] = wrapFunction(impl);
else object[property] = wrapFunction(function() { return impl; });
object[property].restore = function() {
if (!_.isUndefined(original)) object[property] = original;
else delete object[property];
};
return object[property];
}
function wrapFunction(fn) {
const properties = _.mapObject({
get callCount() { return calls.length; },
get called() { return calls.length > 0; },
get notCalled() { return calls.length === 0; },
// etc...
}, function(v, k, o) { return Object.getOwnPropertyDescriptor(o, k); });
const calls = [];
const wrappedFn = function() {
const args = Array.prototype.slice.call(arguments);
var error;
var returnVal;
try { returnVal = fn.apply(this, args); }
catch (e) { error = e; }
calls.push({
thisObj: this,
params: args,
error: error,
returnVal: returnVal,
});
return returnVal;
};
Object.defineProperties(wrappedFn, properties);
return wrappedFn;
}
Then, my test:
test('baseFontSize', function() {
const propService = {};
stub(propService, 'getDocumentProperties', fakeGetProperties());
stub(propService, 'getUserProperties', fakeGetProperties());
equal(baseFontSize(propService), 11);
});
//...
function fakeGetProperties() {
const map = {};
const container = {
deleteAllProperties: function() {_.each(map, function(v, k){depete map[k];});},
deleteProperty: function(k) { delete map[k]; },
getKeys: function() { return Object.keys(map); },
getProperties: function() { return _.clone(map); },
getProperty: function(key) { return map[key] || null; },
setProperties: function(obj, deleteOthers) {
if (deleteOthers) container.deleteAllProerties();
_.each(obj, function(v, k) { map[k] = v; });
},
setProperty: function(key, value) { map[key] = value; },
};
return function() { return container; };
}
And the edited version of my function to test, with DI for PropertiesService:
function baseFontSize(propService) {
if (_.isUndefined(propService)) {
propService = PropertiesService;
}
var baseFontSize = JSON.parse(
propService.getDocumentProperties().getProperty('baseFontSize'));
if (baseFontSize === null) {
baseFontSize = JSON.parse(
propService.getUserProperties().getProperty('baseFontSize'));
if (baseFontSize === null) {
propService.getUserProperties().setProperty('baseFontSize', '11');
baseFontSize = 11;
}
propService.getDocumentProperties()
.setProperty('baseFontSize', JSON.stringify(baseFontSize));
}
return baseFontSize;
}