unit-testinggoogle-apps-scriptjestjstddclasp

How to mock dependencies when testing GAS with CLASP


Background

I recently learned about CLASP and became excited about the possibility of using TDD to edit my Google Apps Scripts (GAS) locally.

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 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)

Challenge

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, respectively--see below for an illustration of this.

Related StackOverflow Post

Although there is a similar SO question on unit testing here, most of the content/comments appear to be from the pre-clasp era, and I was unable to arrive at a solution while following up the remaining leads. (Granted, it's very possible my untrained eye missed something!).

Attempts

Using gas-local

As I mentioned above, I created an issue (see link above) after trying to mock multiple dependencies while using gas-local. My configuration was similar to the jest.mock test I describe below, though it's worth noting the following differences:

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

Result (after running jest command)

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)

For reference, if I go to the google-apps-script.spreadsheet.d.ts file that has the types I want, I see the following declarations at the top of the file...

declare namespace GoogleAppsScript {
  namespace Spreadsheet {

...and this one at the bottom of the file:

declare var SpreadsheetApp: GoogleAppsScript.Spreadsheet.SpreadsheetApp;

So maybe I am just importing SpreadsheetApp incorrectly?

Other files

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

Solution

  • Note: the scope of your question is broad and may require clarification.

    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)

    You don't need Jest or any particular testing framework to mock the global Apps Script objects.

    // 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?

    Yes, it is incorrect to import .d.ts into Jest. Jest doesn't need the TypeScript file for SpreadsheetApp. You can omit it. You only need to slightly modify the above example for 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 does not contain modules and you do not import them. These are TypeScript declaration files. Your editor, if it supports TypeScript, will read those files in the background and suddenly you'll have the ability to get autocomplete, even in plain JavaScript files.

    Additional comments