使用 CLASP 测试 GAS 时如何模拟依赖关系

How to mock dependencies when testing GAS with CLASP

背景

我最近在本地了解到 CLASP and became excited about the possibility of using TDD to edit my Google Apps Scripts (GAS)。

NOTE: there might be a way to write tests using the existing GAS editor, but I'd prefer to use a modern editor if at all possible

clasp 工作得很好,但我不知道如何为单元测试模拟依赖项(主要通过 jest,尽管我很乐意使用任何有效的工具)

挑战

尽管安装了 @types/google-apps-script,但我不清楚如何“要求”或“导入”Google Apps 脚本模块分别使用 ES5 还是 ES2015 语法——请参见下面的说明这个。

相关 Whosebug Post

虽然单元测试也有类似的SO问题here,但大部分content/comments貌似是pre-clasps时代的,我跟着也没能得出解决方案剩下的线索。 (当然,我未经训练的眼睛很可能漏掉了什么!)。

尝试次数

使用 gas-local

正如我上面提到的,在使用 gas-local 尝试模拟多个依赖项后,我创建了一个问题(参见上面的 link)。我的配置类似于我在下面描述的 jest.mock 测试,但值得注意以下差异:

使用jest.mock

LedgerScripts.test.js

import { getSummaryHTML } from "./LedgerScripts.js";
import { SpreadsheetApp } from '../node_modules/@types/google-apps-script/google-apps-script.spreadsheet';

test('test a thing', () => {
    jest.mock('SpreadSheetApp', () => {
        return jest.fn().mockImplementation(() => { // Works and lets you check for constructor calls
          return { getActiveSpreadsheet: () => {} };
        });
      });
    SpreadsheetApp.mockResolvedValue('TestSpreadSheetName');

    const result = getSummaryHTML;
    expect(result).toBeInstanceOf(String);
});

LedgerScripts.js

//Generates the summary of transactions for embedding in email
function getSummaryHTML(){  
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var dashboard = ss.getSheetByName("Dashboard");

  // Do other stuff  
  return "<p>some HTML would go here</p>"
}

export default getSummaryHTML;

结果(在 运行 jest 命令之后)

Cannot find module '../node_modules/@types/google-apps-script/google-apps-script.spreadsheet' from 'src/LedgerScripts.test.js'

      1 | import { getSummaryHTML } from "./LedgerScripts.js";
    > 2 | import { SpreadsheetApp } from '../node_modules/@types/google-apps-script/google-apps-script.spreadsheet';
        | ^
      3 | 
      4 | test('test a thing', () => {
      5 |     jest.mock('SpreadSheetApp', () => {

      at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:307:11)
      at Object.<anonymous> (src/LedgerScripts.test.js:2:1)

作为参考,如果我转到具有我想要的类型的 google-apps-script.spreadsheet.d.ts 文件,我会在文件顶部看到以下声明...

declare namespace GoogleAppsScript {
  namespace Spreadsheet {

...文件底部的这个:

declare var SpreadsheetApp: GoogleAppsScript.Spreadsheet.SpreadsheetApp;

所以也许我只是错误地导入了 SpreadsheetApp

其他文件

jest.config.js

module.exports = {
    
    clearMocks: true,
    moduleFileExtensions: [
      "js",
      "json",
      "jsx",
      "ts",
      "tsx",
      "node"
    ],
    testEnvironment: "node",
  };

babel.config.js

module.exports = {
  presets: ["@babel/preset-env"],
};

package.json

{
  "name": "ledger-scripts",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.11.1",
    "@babel/preset-env": "^7.11.0",
    "@types/google-apps-script": "^1.0.14",
    "@types/node": "^14.0.27",
    "babel-jest": "^26.3.0",
    "commonjs": "0.0.1",
    "eslint": "^7.6.0",
    "eslint-plugin-jest": "^23.20.0",
    "gas-local": "^1.3.1",
    "requirejs": "^2.3.6"
  },
  "devDependencies": {
    "@types/jasmine": "^3.5.12",
    "@types/jest": "^26.0.9",
    "jest": "^26.3.0"
  }
}

注意:您的问题范围很广,可能需要澄清。

clasp works great, but I cannot figure out how to mock dependencies for unit tests (primarily via jest, though I'm happy to use any tool that works)

您不需要 Jest 或任何特定的测试框架来模拟全局 Apps 脚本对象。

// LedgerScripts.test.js
import getSummaryHTML from "./LedgerScripts.js";

global.SpreadsheetApp = {
  getActiveSpreadsheet: () => ({
    getSheetByName: () => ({}),
  }),
};

console.log(typeof getSummaryHTML() === "string");
$ node LedgerScripts.test.js
true

So maybe I am just importing SpreadsheetApp incorrectly?

是的,将 .d.ts 导入 Jest 是不正确的。 Jest 不需要 SpreadsheetApp 的 TypeScript 文件。你可以省略它。 对于Jest,你只需要稍微修改上面的例子。

// LedgerScripts.test.js - Jest version
import getSummaryHTML from "./LedgerScripts";

global.SpreadsheetApp = {
  getActiveSpreadsheet: () => ({
    getSheetByName: () => ({}),
  }),
};

test("summary returns a string", () => {
  expect(typeof getSummaryHTML()).toBe("string");
});

Despite installing @types/google-apps-script, I am unclear on how to "require" or "import" Google Apps Script modules whether using ES5 or ES2015 syntax

@types/google-apps-script 不包含模块,您不导入它们。这些是 TypeScript declaration files。您的编辑器,如果它支持 TypeScript,将在后台读取这些文件,突然间您将能够自动完成,即使是普通的 JavaScript 文件。

补充评论

  • 在这里你检查一个函数 returns 一个字符串,也许只是为了让你的例子非常简单。但是,必须强调的是,这种测试最好留给 TypeScript。
  • 既然您返回了一个 HTML 字符串,我觉得有义务指出 Apps Script 出色的 HTML Service 和模板化能力。
  • 单元测试还是集成测试?您提到了单元测试,但依赖全局变量通常表明您 可能 不是单元测试。考虑重构您的函数,以便它们接收对象作为输入,而不是从全局范围调用它们。
  • 模块语法:如果您使用 export default foo,则导入时不带大括号:import foo from "foo.js" 但如果您使用 export function foo() {,则使用大括号:import { foo } from "foo.js"