clasp の Hello Apps Script のテストコードを書く

# はじめに

弊社では、Googleスプレッドシートに Google Apps Script を組み合わせて使うことがあります。

最近になって、自分も Google Apps Script を使う機会ができたので、使い方を覚えることにします。

以下のことができるようにします。

- clasp を使用する。
- TypeScript を使用する。
- テストコードを Jest で書く。

# 手順

## 初期プロジェクトの作成

### clasp をインストールする

https://github.com/google/clasp

```sh
npm init -y

npm install @google/clasp
```

Google Apps Script API を有効にすることも忘れずに。

### Googleスプレッドシートと Apps Script を作成する

```sh
npx clasp create \
--type sheets \
--title sample20230812-01 \
--rootDir .
```

`--title` で指定した名前の Googleスプレッドシートが、マイドライブに作成されます。

### .claspignore を作成する

```sh
echo "node_modules/**
coverage/**
jest.config.ts
**/*.test.ts
" > .claspignore
```

`.claspignore` ファイルを作成する場合、`node_modules/**` を含めておかないと、`clasp push` がえんえんと実行中になり終わらない...

### eslint をインストールする

https://typescript-eslint.io/getting-started

```sh
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint typescript

echo "/* eslint-env node */
module.exports = {
  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  root: true,
};" > .eslintrc.cjs
```

### Goolge Apps Script の TypeScript 用の設定を追加する

https://github.com/google/clasp/blob/master/docs/typescript.md

```sh
npm i -S @types/google-apps-script

echo '{
  "compilerOptions": {
    "lib": ["esnext"],
    "experimentalDecorators": true
  }
}' > tsconfig.json
```

### Jest をインストールする

https://jestjs.io/ja/docs/getting-started

```sh
npm install --save-dev jest
npm install --save-dev ts-jest
npm install --save-dev @jest/globals
npm install --save-dev @types/jest
npm install --save-dev ts-node

npx jest --init

The following questions will help Jest to create a suitable configuration for your project

✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … yes
✔ Choose the test environment that will be used for testing › node
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › v8
✔ Automatically clear mock calls, instances, contexts and results before every test? … yes

✏  Modified /(プロジェクトのパス)/package.json

📝  Configuration file created at /(プロジェクトのパス)/jest.config.ts
```

「TypeScript Deep Dive」のサイトに記載のある設定を追加します。<br>

~~なお、設定ファイルが `jest.config.js` という名前で作成されますが、ファイル内に記述されている `import type {Config} from 'jest';` という書き方が TypeScript 用の書き方に見えるので、ファイル名を `jest.config.ts` に変えてみます。~~

https://typescript-jp.gitbook.io/deep-dive/intro-1/jest

```sh
mv jest.config.ts jest.config.ts.org

cat jest.config.ts.org | sed -e 's/^};$/  roots: [\n    "<rootDir>"\n  ],\n  testMatch: [\n    "**\/__tests__\/**\/*.+(ts|tsx|js)",\n    "**\/?(*.)+(spec|test).+(ts|tsx|js)"\n  ],\n  transform: {\n    "^.+\\\\.(ts|tsx)$": "ts-jest"\n  },\n};/' > jest.config.ts

# rm jest.config.ts.org
```

js.config.ts に追加される内容は以下。

```ts
  roots: [
    "<rootDir>"
  ],
  testMatch: [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  transform: {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
```

## ソースコードとテストコードを作成する

### ソースコード

たとえば clasp の GitHub で紹介されているデモ用のソースコードの場合。

Google Apps Script で定義されている `Logger.log()` が使用されており、テストを実行するために少々工夫します。

```ts:src/hello.ts
function hello(): void {
  Logger.log("Hello, Apps Script!");
}

module.exports = hello;
```

確認用に、関数の戻り値の型を書いてみました。

TypeScript 構文が読み取られる設定が正しく行われていなければ、`:void` の部分で構文エラーになります。そうなったら、何かを間違えているはず。エラーがなくなるように頑張るしかない (-_-;

また、テストコードから関数を参照できるようにするために、`module.exports` を追加しました。

### テストコード

```tests/hello.test.ts
import { describe, expect, test } from '@jest/globals';

const hello = require('../src/hello');

// Logger をモックするためのクラスを定義する
class LoggerMock implements GoogleAppsScript.Base.Logger {
  private lastLog: string;
  clear (): void { this.lastLog = undefined; }
  getLog (): string { return this.lastLog; }
  log (data: string): LoggerMock { this.lastLog = data; return this; }
}

describe('テストです', ()=> {
  beforeEach(() => {
    // Logger にインスタンスをセットする
    global.Logger = new LoggerMock();
  });
  
  test('ログ出力に成功すること', () => {
    hello();
    expect(global.Logger.getLog()).toBe('Hello, Apps Script!');
  });
});
```

Google Apps Script で `Logger` という変数が定義されていますが、テストの実行時は値が undefined になっているので、代わりのオブジェクトを代入します。

global 名前空間の変数の値を書き換えるとは、なんてことをしてくれるんだ、というアンチパターンなのかもしれませんが、そもそも値が undefined なのだから、まぁいいか...

### テストを実行する

```sh
npm run test

 PASS  tests/hello.test.ts
  テストです    ✓ ログ出力に成功すること (2 ms)
```

## リモートにプッシュして実行する

```sh
npx clasp login
```

```sh
npx clasp push
```

プッシュに成功したら、Google Apps Script のエディタ画面で実行してみて、スクリプトの実行が成功するかを確認します。

![20230812-01_AppsScriptLog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/97e6fe84-7869-09b5-da11-e977f5197754.png)


# おわりに

Google Apps Script を TypeScript で書きつつ、テストコードの実行ができました。

# 参考

- [google/clasp](https://github.com/google/clasp)
- [typescript-eslint](https://typescript-eslint.io/)
- [TypeScript Deep Dive 日本語版](https://typescript-jp.gitbook.io/deep-dive/)
- [Jest](https://jestjs.io/ja/)
- [Jestでglobalなオブジェクトをmockしたテストを書く](https://qiita.com/mistolteen/items/76c335107f9859cd537f)
- [Google Apps Script でも テスト がしたい！ (Clasp + Typescript + Jest)](https://silverbirder.net/entry/2020/02/01/235534)