DEV Community

Dave Parsons
Dave Parsons

Posted on

How to modularize and test Google Apps Scripts

Overview

Google Apps Script (GAS) is the mechanism for adding additional functionality to Google Docs, Sheets, Calendar, and other products. For example, I wrote a script that synchronizes Google Calendar events with rows in a Google Sheet. It's fairly easy to write a short script to automate many tasks in Google's apps. However, until recently it was fairly difficult to make more complicated scripts due to a lack of module support.

Over the past year Google has introduced two huge features to GAS development, Typescript (TS) support (currently pegged at TS 3.5.2) and switching to the V8 runtime and thus modern ECMAScript. Prior to these updates scripts had to be written in a very old version (ES5) of Javascript. While these two changes bring many improvements (arrow functions, classes, static type checking, etc.) there are a couple of unexpected behaviors when using modules. I'll discuss these and offer work arounds.

Setup

Setting up Typescript for GAS is straightforward and well documented, so I won't repeat it here. You'll need to install npm if you don't have it already, and then the "clasp" tool that does the Typescript conversion along with some type definitions. More instructions guide you in either creating a new script project or cloning one that already exists. You write your code in .ts files and they will be converted to Javascript and uploaded into your script project as corresponding .gs files (gs for Google Script?).

Using TS Modules

A typical GAS script includes top-level functions that the Google infrastructure will call in response to events like opening a Sheet (onOpen) or installing the add-on (onInstall). Your script can install a menu that will also call top-level functions using any name you choose. I put these functions in a file named Code.ts based on the convention that GAS code lives in a file called Code.gs.

I also wanted some helper functions for a settings dialog and a class for helping convert between Calendar events and Sheet rows. I put these in separate files, which in Typescript defines separate modules. However, and this is important, during clasp translation the modules will be stripped away. All of the code from all of your .ts files will live in one global namespace. It's just like having all of the code merged together into one file. You don't need (and can't have) import statements.

Testing issues

I wanted to maintain reliability while developing some new features, so I decided to add some tests. The lack of module support presented some problems. I created a tests directory, wrote some tests, and installed Jasmine to run them. I added the tests directory to the .claspignore file so they wouldn't compile and upload as part of the script project, and thus I could use import to include the file under test. Great! However, my code modules depend on each other. Without import statements the tests failed to compile.

My work around is admittedly hacky and only works on Linux. (I welcome suggestions.) I wrote two scripts pretest:

#!/bin/bash
for f in *.ts; do
  sed -i 's|/\*%|/\*% \*/|;s|%\*/|/\* %\*/|' $f
done
Enter fullscreen mode Exit fullscreen mode

And posttest:

#!/bin/bash
for f in *.ts; do
  sed -i 's|/\*% \*/|/\*%|;s|/\* %\*/|%\*/|' $f
done
Enter fullscreen mode Exit fullscreen mode

When run before and after testing, they comment and uncomment the tags /*% and %*/. I then wrote the import statements for my module dependencies like this:

/*% import {Util} from './Util'; %*/
Enter fullscreen mode Exit fullscreen mode

And then tied it all together with the following configuration in package.json:

  "scripts": {
    "pretest": "./pretest",
    "posttest": "./posttest",
    "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json || :"
  },
Enter fullscreen mode Exit fullscreen mode

This hides the import statements from clasp but enables them for testing making it easy to run the tests with npm test.
Note: The || : at the end of the test command hides the Jasmine exit code. That ensures that posttest always runs even when the tests fail. SO hat tip

Syntax checking issues

The next problem I encountered is that using clasp to translate and upload the TS code does not perform static analysis. For example, if you have a typo in a variable name the error won't become apparent until you upload the code and run it. Using a smart editor like VSCode will highlight at least some of these problems. A more thorough solution is to run a test against the code which executes a full static analysis. One problem with this approach is that I did not have any tests for the main file Code.ts. It primarily calls GAS functions and I didn't have time to write test fakes for all of them. My solution was to add a small dummy function in Code.ts:

export function exerciseSyntax() {
  return true;
}
Enter fullscreen mode Exit fullscreen mode

And then write a small test in Code_test.ts:

export function exerciseSyntax() {
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Now running the tests will catch any issues in all of the files, including Code.ts.

Future Improvements

The GAS script I wrote gets a fair amount of usage, especially from teachers using it for class schedules. I get a steady stream of feature requests that I work on occasionally. I might also write some test fakes for the GAS entry points in order to improve the code coverage for the tests.

Let me know in the comments if this article was useful for you, and if you'd like to see future topics such as how to save and load user settings, the (arduous) process of publishing a script, the ins and outs of scripting Google Calendar, or another topic.

Top comments (0)