QUnit 测试依赖文档属性的函数

QUnit testing a function which leans on document properties

我正在尝试为专为 Google Docs 设计的 Apps 脚本插件编写一些单元测试。一些函数我想对调用 PropertiesService.getDocumentProperties() 进行单元测试。我的附加组件中的一个简单示例函数:

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;
}

我正在使用 QUnit 为 Google Apps 脚本库编写测试:

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...
}

由于测试套件不在文档中 运行,因此没有文档属性。似乎测试我的函数的唯一方法是模拟 getDocumentProperties 函数。当然,我能找到的唯一 Apps 脚本 mock/stub 库要么是为了在 Node.js 环境中进行测试,要么是不够完整,无法满足我的需求,这意味着我必须自己动手。

虽然我仍然希望找到一个更优雅的解决方案,但在此之前我已经拼凑了一个基于 SinonJS API 的简单存根框架,并将存根注入到我要测试的函数中(因为我实际上不能存根 PropertiesService,因为它被冻结了)。

大致来说,我的解决方案(我的项目已经包含 Underscore,所以我尽可能地利用它):

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;
}

然后,我的测试:

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; };
}

以及我要测试的函数的编辑版本,使用 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;
}