How to modularize and test Google Apps Scripts
Tags: typescript, googlecloud
Published: 2020-08-17 03:00:35

# Overview

[Google Apps Script](https://www.google.com/script/start/) (GAS) is the mechanism for adding additional functionality to Google Docs, Sheets, Calendar, and other products. For example, I wrote a [script](https://github.com/Davepar/gcalendarsync) 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](https://developers.google.com/apps-script/guides/typescript) (currently pegged at TS 3.5.2) and switching to the [V8 runtime](https://developers.google.com/apps-script/guides/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](https://developers.google.com/apps-script/guides/typescript), 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](https://developers.google.com/apps-script/guides/clasp#create_a_new_apps_script_project) 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](https://jasmine.github.io/) 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
```

And `posttest`:
```
#!/bin/bash
for f in *.ts; do
  sed -i 's|/\*% \*/|/\*%|;s|/\* %\*/|%\*/|' $f
done
```

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'; %*/
```

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 || :"
  },
```

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](https://stackoverflow.com/questions/25292344/npm-posttest-doesnt-trigger-if-npm-test-fails)

# 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;
}
```

And then write a small test in `Code_test.ts`:
```
export function exerciseSyntax() {
  return true;
}
```

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.