Docker+Clasp+Jest+GitHub ActionsでモリモリGAS開発

本記事は、[サムザップ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/sumzap) の12/4の記事です。

#はじめに
はじめまして、サムザップの[北島](https://twitter.com/RyotoKitajima)です。

突然ですが、Google Apps Script（以降、GAS）で色々している皆さん！
GASは便利ですが、ブラウザのエディター上で開発するのはちょっと大変ですよね。
そこで、今回は [Docker](https://www.docker.com/) + [Clasp](https://github.com/google/clasp) で開発環境を作りつつ、さらに [Jest](https://jestjs.io/ja/) + [GitHub Actions](https://github.co.jp/features/actions) でテストもしてしまえ！という内容です。

というわけで、Docker、Clasp、Jest、GitHub Actionsと、4つについてそれぞれ書いていきます。
が、詳しく書いていくとかなり長くなってしまうため、今回はポイントを絞って簡潔にという感じにしております、ご了承ください。

#開発環境

まず、今回作る開発環境の構成は以下になります。

```
| - gas
|    | - .github
|    |    | - workflows
|    |    |    | - ci.yml
|    | - .docker
|    |    | - Dockerfile
|    |    | - docker-compose.yml
|    | - src
|    |    | - package.json
|    |    | - babel.config.js
|    |    | - node_modules
|    |    | - hoge
|    |    |    | - .clasp.json
|    |    |    | - .claspignore.json
|    |    |    | - appsscript.json
|    |    |    | - hoge.ts
|    |    |    | - __test__
|    |    |    |    | - hoge.test.ts
```

#Docker

では、Docker環境の構築についてです。
Dockerをインストールした状態で、以下のファイルを用意してください。
なお、後に出てくる Clasp、Jest の設定もここでしてしまいます。

**.docker/Dockerfile**

```
FROM node:16.4.1
RUN npm i @google/clasp -g
```

**.docker/docker-compose.yml**

```
version: '3'
services:
  gas:
    build: .
    tty: true
    container_name: 'gas-container'
    volumes:
      - ../src/:/opt/
    working_dir: /opt
```

**src/package.json**

```
{
  "dependencies": {
    "@google/clasp": "^2.4.1",
    "google-spreadsheet": "^3.1.15",
    "npm": "^7.20.6"
  },
  "devDependencies": {
    "@babel/cli": "^7.14.8",
    "@babel/core": "^7.15.0",
    "@babel/plugin-transform-modules-commonjs": "^7.15.0",
    "@babel/preset-env": "^7.15.0",
    "jest": "^27.0.6",
    "jest-junit": "^12.2.0"
  },
  "jest": {
    "globals": {
      "SpreadsheetApp": {}
    }
  }
}
```

**src/babel.config.js**

```
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        },
      }
    ]
  ],
  plugins: [
    '@babel/plugin-transform-modules-commonjs',
  ]
}
```

用意ができたら、以下のコマンドで環境を立ち上げることができます。

**初回のみ**

```
$ cd .docker

$ docker-compose build --no-cache
$ docker-compose up -d

$ docker-compose ps

  ※以下があることを確認
  Name        Command              State  Ports
  -----------------------------------------------------
  gas-container   docker-entrypoint.sh node   Up

（停止するときは以下）
$ docker-compose down
```

**初回以降**

```
$ cd .docker
$ docker-compose up -d

（停止するときは以下）
$ docker-compose down
```

#Clasp

続いて、Claspについてです。

Claspは、Google が提供するGASをローカルで管理できるようになるツールになります。
素直にGASの開発を行うと、ブラウザ上のエディターにコードを書くことになりますが、Claspを使うとローカルで書いたものを `clasp push` で簡単に反映できるようになります。

上記のDocker環境を立ち上げた状態で以下を実行します。

```
$ docker-compose exec gas bash

$ npx clasp login --no-localhost

  ※URLが表示されるのでブラウザでアクセスしGoogleアカウントでログイン
  ※codeが表示されるのでコピペ
```

これにより ``.docker/.clasprc.json`` が作成され、以降はGoogleへのログインが不要になります。
次に ``.clasp.json`` を用意します。

**src/hoge/.clasp.json**

```
{
  "scriptId":"【管理したいGASのID】"
}
```

Claspで管理したいGASは予め作成しておいてください。
これで以下のコマンドが実行可能になります。

```
$ npx clasp pull

  GASに反映されているコードをローカルに持ってきます

$ npx clasp open

  GASをブラウザで開きます

$ npx clasp push

  ローカルのコードをGASに反映します

```

ローカルでの開発が可能になりましたね！

#Jest

続いて、Jestです。
Jestのインストールは以下でできます。

```
$ docker-compose exec ikusa-gas bash
$ npm install --save-dev jest
```

Jestを使うことで、GASのテストコードを書くことができます。
コードは ``__test__`` 配下に置くか、ファイル名を ``hoge.test.ts`` とします。

**テストするコードのサンプル（src/hoge/hoge.ts）**

```
/*
 * 足し算
 */
function plus(a, b) {
  return add(a, b);
}

/*
 * スプレットシートの範囲を指定して取得、配列で返す
 */
function getSheetAsArray(ssUrl, ssName, startRow, endRow, startColumn, endColumn) {
  var sheet = SpreadsheetApp.openByUrl(ssUrl+"\/").getSheetByName(ssName);
  var result  = sheet.getRange(startRow, startColumn, endRow-startRow+1, endColumn-startColumns+1).getValues();

  return result;
}
```

**テストコードのサンプル（src/hoge/\_\_test\_\_/hoge.test.ts）**

```
import {plus, getSheetAsArray} from '../hoge.ts';

// モックの例
SpreadsheetApp.openByUrl = jest.fn(() => ({
  getSheetByName: jest.fn(() => (
    {
      getRange: jest.fn(() => (
        {
          getValues: jest.fn(() => [
            ["HEAD1", "HEAD2", "HEAD3"],
            ["A1", "B1", "C1"],
            ["A2", "B2", "C2"],
          ]),
        }
      )),
    }
  )),
}));

// テストの例
describe('テスト', () => {

  it('plus', () => {
    var result = plus(2, 3);
    expect(result).toEqual(5);
  });

  it('# getSheetAsArray', () => {
    var output = getSheetAsArray();
    expect(output[0][0]).toBe("HEAD1");
    expect(output[0][1]).toBe("HEAD2");
    expect(output[0][2]).toBe("HEAD3");
    expect(output[1][0]).toBe("A1");
    expect(output[1][1]).toBe("B1");
    expect(output[1][2]).toBe("C1");
    expect(output[2][0]).toBe("A2");
    expect(output[2][1]).toBe("B2");
    expect(output[2][2]).toBe("C2");
  });

});
```

こんな感じで簡単にテストが書けます。
GAS開発の場合、スプレットシートを使うことが多いと思います。
スプレットシートを操作するような関数は、上記のようにモック化すると良いと思います。

テストを実行する方法は以下のとおりです。

```
すべてのテストを実行する場合
  $ docker-compose exec gas npx jest

管理するGASが複数になり、特定のテストだけを実行する場合
例）src/hoge2 配下のテストを実行する場合
  $ docker-compose exec gas npx jest --verbose hoge2
```

なお、テストコードはGASに上げる必要はないので ``.claspignore`` を設定すると良いです。

**src/hoge/.claspignore**

```
__test__/*
```

#GitHub Actions

最後に、GitHub Actionsについてです。
上記ではテストを手動で実行しましたが、GitHub Actionsを使うことで自動で実行することができます。
（GASのコードをGitHubでバージョン管理している前提です。）
masterブランチへのプルリクが作成されたタイミングでテストを実行したい場合を例として載せておきます。

**.github/workflows/ci.yml**

```
name: CI

on:
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - name: git clone
        uses: actions/checkout@v1
      - name: Instal node
        uses: actions/setup-node@v1
        with:
          node-version: '16.4.1'
      - name: Install npm
        run: npm install -g npm@7.18.1
      - name: Install jest
        run: npm install --save-dev jest
        working-directory: ./src
      - name: Install babel
        run: npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-modules-commonjs
        working-directory: ./src
      - name: Run test
        run: npx jest
        working-directory: ./src
```

#まとめ

というわけで、今回は Docker + Clasp + Jest + GitHub Actions でGAS開発をモリモリにする方法を紹介しました。
日頃、GASで開発をしている皆さんの参考になれば嬉しいです。

以上、[サムザップ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/sumzap) の12/4の記事でした。
明日は [@fuzy](https://qiita.com/fuzy) さんの記事です、お楽しみに！