Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)

  • I got farthest by using the gas-local package, and was able to mock a single dependency within a test
    • However I could not find a way to mock multiple dependencies in a single test/call, and so I created this issue

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:

  • I used ES5 syntax for the gas-local tests
  • My package configuration was probably slightly different

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"
  }
}
like image 338
James Tasse Avatar asked Aug 15 '20 17:08

James Tasse


Video Answer


1 Answers

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

  • Here you check that a function returns a string, perhaps just to make your example very simple. However, it must be stressed that such testing is better left to TypeScript.
  • Since you returned an HTML string, I feel obligated to point out the excellent HTML Service and templating abilities of Apps Script.
  • Unit testing or integration testing? You mention unit testing, but relying upon globals is generally a sign you might not be unit testing. Consider refactoring your functions so they receive objects as input rather than calling them from the global scope.
  • Module syntax: if you use export default foo, you then import without curly braces: import foo from "foo.js" but if you use export function foo() { then you use the curly braces: import { foo } from "foo.js"
like image 200
dwmorrin Avatar answered Nov 07 '22 13:11

dwmorrin