はじめに
Google Apps Scriptを開発するときにclaspを導入してソース管理し始めた後、もっと色々できないかと調べた結果を備忘録として記載します。
GOAL
開発環境として最終的に下記を利用して開発できるようになります。
- VSCode: コードエディター
- clasp: Google Apps Scriptをローカルで開発できるようにするもの
- TypeScript: プレーンJavaScriptにコンパイルされるJavaScriptの型付きスーパーセット
- ESLint: JavaScriptコードの問題を見つけて修正することができる
- Jest: シンプルさを重視した楽しいJavaScriptテストフレームワーク
claspのインストール〜プロジェクト作成
https://github.com/google/clasp
上記ページのInstallセクションの記載通りに実行します。
$ npm install -g @google/clasp
ログインします。初回だとブラウザが起動してclaspへのアクセス許可をもとめられるので許可すると~/.clasprc.json
に認証情報が保存されます。
$ clasp login
プロジェクトを作成します。今回は、standaloneプロジェクトを作成します。
$ mkdir gas-example
$ cd gas-example
$ clasp create --type standalone
Created new standalone script: https://script.google.com/d/xxxxxxxxxx/edit
Warning: files in subfolder are not accounted for unless you set a '.claspignore' file.
Cloned 1 file.
└─ appsscript.json
Typescriptのインストール
最初にpackage.jsonを作成します。色々質問されますが、とりあえず、全部EnterでOK。
$ npm init
typescript関連のパッケージをインストールします。
$ npm install --save-dev typescript ts-node @types/node
https://github.com/google/clasp/blob/master/docs/typescript.md
上記ページのPrerequisitesセクションの記載通りに実行します。
$ npm i -S @types/google-apps-script
tsconfig.jsonというファイルを作成して、TypeScript機能を有効にします。
ソースコードは、src/
に配置し、テストコードはtest/
に配置することにします。
{
"compilerOptions": {
"lib": ["esnext"],
"experimentalDecorators": true
},
"include": [
"src/**/*",
"test/**/*"
]
}
.clasp.json
も下記の通りに修正します。
{
"scriptId":"xxxxxxxxx",
"rootDir": "src/",
"fileExtension": "ts"
}
appsscript.json
ファイルもsrc/
に移動します。
$ mkdir src test
$ mv appsscript.json src/
ESLint & Prettierのインストール
https://eslint.org/docs/user-guide/getting-started
上記ページのInstallation and Usageセクションの記載通りに実行します。
$ npm install eslint --save-dev
次に.eslintrc.json
をセットアップします。
$ npx eslint --init
? How would you like to use ESLint?
To check syntax, find problems, and enforce code style
? What type of modules does your project use?
None of these
? Which framework does your project use?
None of these
? Does your project use TypeScript?
Yes
? Where does your code run?
Node
? How would you like to define a style for your project?
Use a popular style guide
? Which style guide do you want to follow?
Standard: https://github.com/standard/standard
? What format do you want your config file to be in?
JSON
Checking peerDependencies of eslint-config-standard@latest
The config that you've selected requires the following dependencies:
@typescript-eslint/eslint-plugin@latest eslint-config-standard@latest eslint@>=6.2.2 eslint-plugin-import@>=2.18.0 eslint-plugin-node@>=9.1.0 eslint-plugin-promise@>=4.2.1 eslint-plugin-standard@>=4.0.0 @typescript-eslint/parser@latest
? Would you like to install them now with npm?
Yes
Installing @typescript-eslint/eslint-plugin@latest, eslint-config-standard@latest, eslint@>=6.2.2, eslint-plugin-import@>=2.18.0, eslint-plugin-node@>=9.1.0, eslint-plugin-promise@>=4.2.1, eslint-plugin-standard@>=4.0.0, @typescript-eslint/parser@latest
https://prettier.io/docs/en/install.html
https://github.com/prettier/prettier-eslint
https://github.com/prettier/eslint-config-prettier
https://github.com/prettier/eslint-plugin-prettier
上記ページを参考にしながらprettier関連パッケージをインストールします。
$ npm install --save-dev --save-exact prettier
$ npm install --save-dev prettier-eslint eslint-config-prettier eslint-plugin-prettier
.eslintrc.json
のextendsの部分を修正します。
{
"env": {
"es6": true,
"node": true
},
"extends": [
"standard",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}
Jest & ts-jestのインストール
https://jestjs.io/docs/ja/getting-started
上記ページのGetting Startedセクションの記載通りに実行します。
$ npm install --save-dev jest
https://github.com/kulshekhar/ts-jest
上記ページのGetting Started - Installingセクションを参考にし、下記を実行します。
$ npm i -D ts-jest @types/jest
次にjest.config.js
ファイルを生成します。
$ npx ts-jest config:init
hello.ts & hello.test.tsの作成
https://github.com/google/clasp/blob/master/docs/typescript.md
のhello.ts
をベースにsrc/hello.ts
、test/hello.test.ts
を作成します。
const greeter = (person: string): string => {
return `Hello, ${person}!`;
};
function testGreeter(): string {
const user = "Grant";
const msg = greeter(user);
Logger.log(msg);
return msg;
}
export { greeter, testGreeter };
import { greeter, testGreeter } from "../src/hello";
describe("test.ts test", () => {
beforeAll(() => {
Logger.log = jest.fn().mockImplementation(msg => {
return console.log(msg);
});
jest.spyOn(Logger, "log");
});
test("greeter", () => {
const person = "World";
const expected = greeter(person);
expect(expected).toBe("Hello, World!");
});
test("testGreeter", () => {
const expected = testGreeter();
expect(Logger.log).toBeCalled();
expect(expected).toBe("Hello, Grant!");
});
});
GoogleAppScriptで定義されているLogger.log
をjestでモック化するためにjest.config.js
を下記の通り修正します。
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
globals: {
Logger: {}
}
};
テストコードの実行
下記コマンドでテストコードが実行できます。
Logger.log
の代わりにconsole.log
が実行されていることがわかります。
$ npx jest
PASS test/hello.test.ts
test.ts test
✓ greeter (2ms)
✓ testGreeter (8ms)
console.log test/hello.test.ts:6
Hello, Grant!
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.821s
Ran all test suites.
console.log
を出力したくない場合
$ npx jest --silent=true
PASS test/hello.test.ts
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.991s, estimated 3s
カバレッジを出力したい場合
$ npx jest --silent=true --coverage
PASS test/hello.test.ts
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
hello.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.7s
Apps Scriptを実行する
typescriptファイルをapps scriptサーバにpushします。
$ clasp push
└─ src/appsscript.json
└─ src/hello.ts
Pushed 2 files.
script.google.comでApps Scriptプロジェクトを開きます。
$ clasp open
Opening script: https://script.google.com/d/xxxxxxxxxx/edit
ブラウザが開くのでtestGreeter()
を実行し、[表示]> [ログ]を押してログを表示することで、結果を確認できます。
hello.ts
は、下記のようにGoogle Apps Scirptに変換されていることがわかります。
// Compiled using ts2gas 3.4.4 (TypeScript 3.7.2)
var exports = exports || {};
var module = module || { exports: exports };
var greeter = function (person) {
return "Hello, " + person + "!";
};
exports.greeter = greeter;
function testGreeter() {
var user = "Grant";
var msg = greeter(user);
Logger.log(msg);
return msg;
}
exports.testGreeter = testGreeter;
おまけ:VSCodeの設定
ファイルセーブ時に自動的にESLintで自動整形できるように設定する。
{
"eslint.alwaysShowStatus": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
}
デバッグ実行の設定
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"args": [
"${relativeFile}"
],
"runtimeArgs": [
"--nolazy",
"-r",
"ts-node/register"
],
"sourceMaps": true,
"cwd": "${workspaceRoot}",
"protocol": "inspector"
},
{
"type": "node",
"request": "launch",
"name": "Debug Tests",
"args": [
"${relativeFile}"
],
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js",
"--runInBand",
"--silent=true",
"-o"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
}
]
}
#おまけ2:package.jsonの設定
package.json
を下記のように修正することでnpm run
で実行できるようになります。
〜省略〜
"scripts": {
"ts-node": "npx ts-node",
"test": "npx jest --silent=true --coverage",
"test-only": "npx jest --silent=true -o"
},
〜省略〜
テスト実行
$ npm run test
> gas-example@1.0.0 test /Users/xxxxx/gas-example
> npx jest --silent=true --coverage
PASS test/hello.test.ts
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
hello.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.227s