如何在 GAS 中测试触发函数?

How can I test a trigger function in GAS?

提问人:Mogsdad 提问时间:4/19/2013 最后编辑:TheMasterMogsdad 更新时间:10/25/2021 访问量:40115

问:

Google Apps 脚本支持触发,用于将事件传递给触发器函数。遗憾的是,开发环境将允许您在不传递参数的情况下测试函数,因此您无法以这种方式模拟事件。如果尝试,则会出现如下错误:

ReferenceError: 'e' 未定义。

TypeError:无法从未定义中读取属性 *...*

(其中未定义)e

可以将事件视为可选参数,并使用 Is there a better way to do optional function parameters in JavaScript?中的任何技术将默认值插入触发器函数中。但这会带来一个风险,即一个懒惰的程序员(如果是你的话,请举手!)会留下这些代码,并产生意想不到的副作用。

肯定有更好的方法吗?

调试 google-apps-script 触发器

评论

0赞 TheMaster 9/22/2020
始终浏览所有答案并找到最新的更新,而不仅仅是接受的更新。当前(2020)更新在这里

答:

95赞 Mogsdad 4/19/2013 #1

您可以编写一个测试函数,将模拟事件传递给触发器函数。下面是一个测试触发器函数的示例。它传递一个事件对象,其中包含了解事件中为“电子表格编辑事件”描述的所有信息。onEdit()

要使用它,请在目标函数中设置断点,选择函数并点击 。onEdittest_onEditDebug

/**
 * Test function for onEdit. Passes an event object to simulate an edit to
 * a cell in a spreadsheet.
 *
 * Check for updates: https://stackoverflow.com/a/16089067/1677912
 *
 * See https://developers.google.com/apps-script/guides/triggers/events#google_sheets_events
 */
function test_onEdit() {
  onEdit({
    user : Session.getActiveUser().getEmail(),
    source : SpreadsheetApp.getActiveSpreadsheet(),
    range : SpreadsheetApp.getActiveSpreadsheet().getActiveCell(),
    value : SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getValue(),
    authMode : "LIMITED"
  });
}

如果您好奇,这是为了测试以三个单元格为条件的 Google 电子表格的函数而编写的。onEdit

下面是电子表格表单提交事件的测试函数。它通过读取表单提交数据来构建其模拟事件。这最初是为在 onFormSubmit 触发器中获取 TypeError 而编写的。

/**
 * Test function for Spreadsheet Form Submit trigger functions.
 * Loops through content of sheet, creating simulated Form Submit Events.
 *
 * Check for updates: https://stackoverflow.com/a/16089067/1677912
 *
 * See https://developers.google.com/apps-script/guides/triggers/events#google_sheets_events
 */
function test_onFormSubmit() {
  var dataRange = SpreadsheetApp.getActiveSheet().getDataRange();
  var data = dataRange.getValues();
  var headers = data[0];
  // Start at row 1, skipping headers in row 0
  for (var row=1; row < data.length; row++) {
    var e = {};
    e.values = data[row].filter(Boolean);  // filter: https://stackoverflow.com/a/19888749
    e.range = dataRange.offset(row,0,1,data[0].length);
    e.namedValues = {};
    // Loop through headers to create namedValues object
    // NOTE: all namedValues are arrays.
    for (var col=0; col<headers.length; col++) {
      e.namedValues[headers[col]] = [data[row][col]];
    }
    // Pass the simulated event to onFormSubmit
    onFormSubmit(e);
  }
}

技巧

模拟事件时,请注意尽可能接近记录的事件对象。

  • 如果要验证文档,可以记录从触发器函数接收到的事件。

    Logger.log( JSON.stringify( e , null, 2 ) );
    
  • 在电子表格表单提交事件中:

    • 所有 namedValues 值都是数组。
    • 时间戳是字符串,其格式将本地化为窗体的区域设置。如果从具有默认格式*的电子表格中读取,则它们是 Date 对象。如果触发器函数依赖于时间戳的字符串格式(这是一个坏主意),请注意确保适当地模拟该值。
    • 如果电子表格中的列不在表单中,则此脚本中的技术将模拟包含这些附加值的“事件”,这不是您从表单提交中收到的内容。
    • 问题 4335 中所述,数组跳过空白答案(在“新表单”+“新工作表”中)。该方法用于模拟此行为。valuesfilter(Boolean)

*格式为“纯文本”的单元格会将日期保留为字符串,这不是一个好主意。

评论

0赞 Serge insas 9/16/2014
@Mogsdad : 感谢您注意到数组的事情...我相应地更新了我的脚本;-)顺便说一句,我忘了说我从你那里得到了这个想法,但这篇文章无论如何都是一个“热门”,它不会改变你的生活,不是很抱歉吗,遗漏修复了;-)
1赞 Mogsdad 9/16/2014
@Sergeinsas - 我发誓这些值并不总是数组,也许它们随着新的工作表而改变?或者,也许我以适用于数组的方式使用它们,例如 indexOf() 搜索。无论如何,最好把它做好。
0赞 Serge insas 9/16/2014
我使用的表单提交模拟在没有数组的情况下运行良好......它仍然可以使用它,没有明显的差异。我检查了另一个脚本,其中我编写了真实形式的 Logger 结果,但没有看到数组括号,因此有 2 种可能性:1 我没有戴眼镜 - 2 没有括号......我不能确定哪一个是真的;-)
2赞 Baker 12/4/2018
test_onEdit从 GScript IDE 运行/调试时,会给出错误:“请先选择活动工作表”。我尝试在test_onEdit中打开电子表格并设置活动工作表,但是在调用onEdit时,相同的错误立即停止执行。我是否遗漏了预备步骤?
1赞 Jonathan 6/19/2020
JSON.stringify(e) = 太棒了!
10赞 random-parts 10/21/2017 #2

2017年更新:使用 Google Apps 脚本的 Stackdriver 日志记录调试事件对象。在脚本编辑器的菜单栏中,转到:查看或流式传输日志。View > Stackdriver Logging

console.log() 将写入级别消息DEBUG

示例 onEdit()

function onEdit (e) {
  var debug_e = {
    authMode:  e.authMode,  
    range:  e.range.getA1Notation(),    
    source:  e.source.getId(),
    user:  e.user,   
    value:  e.value,
    oldValue: e. oldValue
  }

  console.log({message: 'onEdit() Event Object', eventObject: debug_e});
}

示例 onFormSubmit()

function onFormSubmit (e) {
  var debug_e = {
    authMode:  e.authMode,  
    namedValues: e.namedValues,
    range:  e.range.getA1Notation(),
    value:  e.value
  }

  console.log({message: 'onFormSubmit() Event Object', eventObject: debug_e});
}

示例 onChange()

function onChange (e) {
  var debug_e = {
    authMode:  e.authMode,  
    changeType: changeType,
    user:  e.user
  }

  console.log({message: 'onChange() Event Object', eventObject: debug_e});
}

然后检查 Stackdriver UI 中标记为字符串的日志以查看输出message

16赞 TheMaster 9/12/2020 #3

2020-2021年更新:

您不需要使用前面答案中建议的任何类型的模拟事件。

如问题中所述,如果直接在脚本编辑器中“运行”该函数,则会出现类似

TypeError:无法从未定义中读取属性...

被抛出。这些不是真正的错误。此错误只是因为您在没有事件的情况下运行了该函数。如果你的函数没有按预期运行,你需要找出实际的错误:

要测试触发函数,

  1. 手动触发相应的事件:即,要测试,编辑工作表中的单元格;要进行测试,请提交虚拟表单响应;若要进行测试,请将浏览器导航到已发布的 Web 应用 URL。onEditonFormSubmitdoGet/exec

  2. 如果有任何错误,则会记录到 stackdriver 中。要查看这些日志,

    • 在脚本编辑器中>左侧栏上的“执行”图标(旧版编辑器:查看>执行)。

    • 或者,单击此处>单击您感兴趣的项目>单击左侧栏上的“执行”图标(第 4 个)

  3. 您可以在执行页面中找到执行列表。确保清除所有过滤器,例如左上角的“Ran as:Me”以显示所有执行。单击您感兴趣的执行,它将以红色显示导致触发器失败的错误。

注意:有时,由于错误,日志不可见。尤其是在 Web 应用程序由匿名用户运行的情况下,情况尤其如此。在这种情况下,建议将默认 Google 云项目切换到标准 Google 云项目,并直接使用 View> Stackdriver 日志记录。有关详细信息,请参阅此处

  1. 若要进一步调试,可以使用编辑代码添加到感兴趣的任何行之后,以查看该对象的详细信息。强烈建议您将要查找的对象字符串化:因为日志查看器具有特殊性。添加后,从步骤 1 开始重复。重复此循环,直到缩小问题范围。console.log(/*object you're interested in*/)console.log(JSON.stringify(e))console.log()

恭喜!你已经成功地解决了问题并越过了第一个障碍。

评论

1赞 Kos 9/30/2021
此解决方案不适合附加组件开发测试,但它更像是附加组件开发问题本身。
0赞 Lexcel Atmadata 8/2/2021 #4

作为上述方法(2020 年更新)第 4 点的补充: 这是我用来跟踪触发代码的一个小例程,它已经为我节省了很多时间。此外,我打开了两个窗口:一个是堆栈驱动程序(执行),另一个是代码(主要驻留在库中),因此我可以轻松找到罪魁祸首。

/**
 *
 * like Logger.log %s in text is replaced by subsequent (stringified) elements in array A
 * @param {string | object} text %s in text is replaced by elements of A[], if text is not a string, it is stringified and A is ignored
 * @param {object[]} A array of objects to insert in text, replaces %s
 * @returns {string} text with objects from A inserted 
 */
function Stringify(text, A) {
  var i = 0 ;
  return (typeof text == 'string') ? 
      text.replace(
        /%s/g, 
        function(m) { 
          if( i >= A.length) return m ;
          var a = A[i++] ;
          return (typeof a == 'string') ? a :  JSON.stringify(a) ;
        } ) 
      : (typeof text == 'object') ? JSON.stringify(text) : text ;
}

/* use Logger (or console) to display text and variables. */
function T(text) {
  Logger.log.apply(Logger, arguments) ;
  var Content = Stringify( text, Array.prototype.slice.call(arguments,1) ) ;
  return Content ;
}

/**** EXAMPLE OF USE ***/
function onSubmitForm(e) {
  T("responses:\n%s" , e.response.getItemResponses().map(r => r.getResponse()) ;
}