TDD of G Suite Add Ons using TypeScript

Extending Google G Suite with Add Ons

Whether it was customer orders, internal automated invoicing, or because someone in our team wanted to undo a configuration error, we here at //Seibert/Media always seem to turn to  Google Apps Script.

It is in our best interest to make the functionality available quickly and easily for the average user.  Google G Suite provides add-ons, especially for this purpose.  Users can just go ahead and install them, and the add-ons can decide where within G Suite they want to add extra buttons, which then perform the desired function.  This is where TDD of G Suite Add Ons using TypeScript comes in.

The need for test-driven development

So far, so good.  The APIs for both the various Google services, as well as the add-ons, are well documented.  There are great examples of DIY tinkering as well as videos that help us to learn a lot in a short period.

Modern software development teams (aka software crafters) often need more:  TDD - Test Driven Development.  Preferably BDD, or Behavior Driven Development.  I won't go into detail here but take a look at some common definitions on the web.

The Challenge: We need specific Entry Points

The issue with that is, that Google Apps Script requires specific Entry Points within the program. For an editor add-on, for example, the functions onOpen and onInstall have to exist. They create the add-on menu that the user should use. Without implementing these named functions, I will not get my functionality to the user.

But now, newer JavaScript standards (and ergo also TypeScript) follow an import/export logic and do not (as before) simply read and evaluate *.js files sequentially until they reach "bottom".  Instead, you import functions and classes from individual modules into other modules.

Although this is not yet the only allowed behavior in all browsers, Google Apps Scripts now have a V8 environment as their default engine (instead of the former Rhino engine from Mozilla).

New JavaScript standards require the export of functions or classes

This particular V8 engine currently does not support any modules. This is, at the very least impractical, and at worst disastrous for a clean TDD approach. If I want to develop my test-driven add-on, I probably want to put tests and source code into different files. Furthermore, the add-on is often complicated enough that different functionalities should also be put into separate files to provide clarity.

But in the newer JavaScript versions, this requires me to export functions or classes to have them available in the other files. As soon as I do this, a trans/compiler recognizes this file as a module file. This again doesn't let me use functionality from elsewhere without import. What to do?

Naive Solution Approach: Stick to window

Now I could easily just go ahead and stick my functionality onto  window. In this way, the function should be globally know - that should satisfy the apps script gods, right?

import { onInstall } from './install.trigger';
import { onOpen } from './open.trigger';
import { myFunction } from './myLogic';

declare global {
    interface Window {
        onInstall:  (e: { authMode: GoogleAppsScript.Script.AuthMode }) => void;
        onOpen:     (e: { authMode: GoogleAppsScript.Script.AuthMode }) => void;
        myFunction: any;
    }
}

window.onInstall  = onInstall;
window.onOpen     = onOpen;
window.myFunction = myFunction; // that I might need because I want them up on the menu

Unfortunately, it's not that easy.  When I compile this TypeScript, (e.g., with Transpiler ts2gas) it comments on myimport-Statements.  Finally, Google apps script doesn't recognize any modules.  If I tried to execute this without  Javascript instead of Transpiler, GAS wouldn't recognize import and export.

If I create a JavaScript bundle (for example, using the Bundlers webpack), all imports and exports within the bundle are resolved - but only at runtime.  This happens because the function names aren't explicitly defined but only made available by the webpack logic at runtime of the script.

As I mentioned at the start, Entry Points must be made visible by a simple lexical check.  If not, the G-suite add-on runtime environment won't be able to handle my code or understand how to retrieve it.  Is this a vicious circle?

The solution: Declare a global layer in TypeScript using the gas-webpack-plugin

Well, no, not really.  It's not without reason that this has all been set up.  Just as I can make global variables visible through TypeScript declarations (even if the compiler doesn't know them), I can also declare a global level where my entry points functions should be appended for example in index.ts like this:

import { onInstall } from './install.trigger';
import { onOpen } from './open.trigger';
import { myFunction } from './myLogic';

declare let global: any;

global.onInstall  = onInstall;
global.onOpen     = onOpen;
global.myFunction = myFunction; // that I might need because I want them up on the menu

Here, the variable  global forms the equivalent of window which used to be used in JavaScript.  However, to make sure that it behaves correctly at runtime, I need the appropriate plugin. For webpack, this would be the gas-webpack-plugin.  Like with everything else, you can get this via npm:

# npm install --save-dev webpack webpack-cli typescript ts-loader gas-webpack-plugin

I can simply add this plugin to my, and the code is executed before the module declarations.  This fills the variable, correctly:

const path = require('path');
const GasPlugin = require('gas-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: './src/index.ts',
    devtool: false,
    output: {
        filename: '[name].js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: 'ts-loader'
            }
        ]
    },
    resolve: {
        extensions: [
            '.ts'
        ]
    },
    plugins: [
        new GasPlugin()
    ]
};

Now that the top-level has been cleared, I can go there and develop my functionalities the way I need them: so they can import each other, including test frameworks like mocha or jest.

Exemplary TDD-Setup

To simplify the administration of my toolchain, it's worth installing some standard packages and defining scripts.  This makes it easy to run the tests whenever you need to.  Here is an example of the combination of mocha as the runner, chai as assertion library und sinon for mocking and stubbing.  But,  jasmine or jest Setups work just as well.

# npm install --save-dev cpx ts-node mocha chai sinon @types/mocha @types/chai @types/sinon

With a few declarations in the  package.json:

"scripts": {
  "build": "webpack && cpx appsscript.json dist && cpx \"src/**/*.html\" dist",
  "deploy": "npm run lint && npm run test && npm run build && clasp push",
  "lint": "tslint -p .",
  "test": "mocha --require ts-node/register --extensions ts 'test/**/*.spec.ts'"
},

If we specify in .clasp.json that "rootDir" : "dist" ,  we can just run  npm test on the command line of our choice and have thus created the prerequisites for starting our first TDD cycle.  Let's write a red test in the directory test/ and create the needed functionality in src/!

Voilá und happy testing!


Further Information

Google Apps Scripts, Clasp and Data Studio
Why large companies and organizations are choosing G Suite


Your partner for the G Suite and Google Cloud solutions

Are you interested in modern collaboration in your company using Google software as an alternative to MS Office? Get in touch if you have any questions or would like to find out more: We are an official Google Cloud Partner and would be happy to give you no-strings advice on the implementation, licensing, and productive use of Google G Suite!

Curious about how Google can help you? Test Google G Suite Free for 30 days!



Learn more about Creative Commons licensing and //Seibert/Media

Forget Less and Ensure Quality with didit Checklists for Atlassian Cloud Forget Less and Ensure Quality with didit Checklists for Atlassian Cloud Forget Less and Ensure Quality with didit Checklists for Atlassian Cloud

Leave a Reply