Google Apps Script開発をもうちょっとモダンにしてみる

よりモダンな開発サイクルへ

前回から早くも1年半の月日が流れてしまったが、今回のテーマは

  • CI/CD と組み合わせてモダンなワークフローの完成形を目指す
  • そのために Node.js ベースのテストフレームワークがちゃんと動くようにする
    • Google サービスのオブジェクトを stub/mock する
  • CoffeeScript をやめて ES2015 へ

の辺り。

前回の記事で残った課題

前回調べた手法を使うと Codegs を利用して Node.js で書きながら 1ソースに build できる。このコードを gas upload すれば GAS 上で動くので、

  • コードを手元の好きなエディタで編集し、
  • Git でバージョン管理し、
  • 適切に module 分割しつつ最終的に GAS で動かす状態に持っていく

ことはできた。ただし、build プロセスが GAS 向けに最適化されており、最終的に全自動にするにはあまり向いていなかった。

gasifyの発見

個人的にはモダン Web 開発の文脈では Browserify と組み合わせてみる実験を

Rails + Browserify + Mithril + cmsxで動くもん書いてみた - あーありがち(2015-09-23)

でしてるんだけど、その後、Google Apps Script 開発方面にこれの影響が来ていたことを知る。

なるほどなるほど。gasify がキモらしい。

fossamagna/gasify: Browserify plugin for Google Apps Script

確かにこれを使うとものすごく手軽に lodash を使うことができた1。これで GAS 用のライブラリになっていない npm の小粒なモジュールも利用可能だ。こりゃーいい。

テスト用ツールチェイン方面の課題

ということで

でいくことにした。

最終的に使ったもの

  • browserify
  • watchify
  • gasify
  • babelify
  • babel
  • babel-preset-es2015
  • babel-core
  • babel-register
  • power-assert
  • intelli-espower-loader
  • mocha
  • sinon
  • gas-manager
  • lodash

こんな感じ。

CI/CDするうえで課題になる認証情報は環境変数で

今回 GAS の開発フローを自動化しようと思ったきっかけは、手離れさせたいのがいちばん強いんだけど、その次は

Google Developers Japan: Apps Script による高度な開発プロセス

だった。Google が公式に紹介するんだから、これまでの課題はいろいろ解消されているに違いないと思ったのだ。

結論から言うと期待は裏切られた。gas-manager でできることとの違いが自分には分からなかった。以前も書いたが、Google Apps Script を使った開発については日本の GAS コミュニティの方が Google 本家より進んでいるんじゃないかと思う。

さて、gas-manager も node-google-apps-script も何が問題かというと、要は認証情報を JSON ファイルで保存する形になってしまっているので、API キーとか ssh の秘密鍵より取り回ししにくいのだ。

Authentication with Google Cloud Platform - CircleCI

を見つけて「おっ」と思ったが、これは gcloud コマンドを使うことが前提の話で、Google Drive API を使う話ではなかった。が、ここにヒントがあった。

自分でやったことは以下の四つ。今回は GitHub + CircleCI で行ったが、考え方は他のツールを使っても変わらない。

  1. gas-manager で credential 情報の JSON を作成、保存する
  2. gas-manager を使って Git + GitHub で開発できる状態に
    • gas-project.json は含む
    • gas-config.json は .gitignore へ
    • npm/yarn run で gas upload / download できるように
  3. CircleCI の project に環境変数を作る GOOGLE_CREDENTIAL とか。これに上の gas-config.json の内容を放り込む
  4. circle.yml で以下のように gas-config.json を生成
dependencies:
  override:
    - echo $GOOGLE_CREDENTIAL > $HOME/$CIRCLE_PROJECT_REPONAME/gas-config.json
    - yarn install

これで gas-manager は -c で生成済み gas-config.json を使うことで、認証情報をリポジトリに保存することなく CI 上で認証を通せる。

gasifyを使ってもimport/exportは使えないのでpresetを調整する

確認したバージョンは gasify 0.1.0

最近 JavaScript復習2017 - importとuse strictとメソッド定義 - - あーありがち(2017-02-19)

で勉強したようにイマドキは import / export だよねーと思って喜んで書き始めたのだが、import / export のところで Script Editor がエラーを出す。2

Google Apps Script は独自仕様の JavaScript 構文 + 独自の JavaScript オブジェクトによるサーバサイドスクリプトで、Babel を通したものでも動かないものはあるということです。これは今度は Babel 側の import / export の処理方法に原因があるので、

Google Apps ScriptでES Module(Babel)を使うときのTips - Qiita

gasify の作者が babel preset を作り始めようとしているようで、もしかしかたらこっちの方が本命になるかもしれないけど、まだ何も中身はない模様 ;-)

を設定します。これは ES 3 時代の文法に適合するように transpile してくれるので import / export が正しく動くようになる、というもののようです。

fossamagna/babel-preset-gas: Babel preset for all Goolge Apps Script plugins.

ということで 2017-05時点では module.exports と require() を使いましょう。

Mocha + babel-register + intelli-espower-loaderは順番に注意

確認したバージョンは

  • mocha 3.4.1
  • intelli-espower-loader 1.0.1
  • babel-register 6.24.1

intelli-espower-loader を先に食わせろ。

具体的なコマンドは以下のようになった。

mocha --require intelli-espower-loader --require babel-register --recursive test

Googleサービス用オブジェクトをSinonでstub out

分かったこと。

  1. gas-local のメリットはよく分からない
  2. Sinon はちゃんと存在しているメソッドがないと stub out できない

テストのサイクルを速くたくさん回すために CI を利用したい、しかし GAS は Google 上の独自サービスあってこそのもの、Node.js だけではすべての機能を動かすことはできない、だから GAS 独自オブジェクトは stub/mock に差し替えてテストを動かす必要がある。

そこで最近は gas-local を使うという話が

Apps Scriptによるより高度な開発プロセス/More Advanced Development Process with Apps Script // Speaker Deck

にあったのだが、gas-local は mock も使えるテストフレームワークと書かれている割にテストの方法についての記述が見つからない。そこでこっちを追うのはやめて、普通に Node.js 向けの代表的なツールチェインで完結させることにした。

上に挙げた mocha のコマンドで以下のものが揃っている。

  • テストランナーは Mocha
  • アサーションは PowerAssert
  • 構文は Babel を使って ES2015 で

そして JS で test double と言えば Sinon だろう。

Sinon.JS - Standalone test spies, stubs and mocks for JavaScript. Works with any unit testing framework.

こいつで stub out してやれば、まぁなんとかなるだろうと思っていたが、ちょっとハマった。

それは

存在しないオブジェクト、存在しないメソッドは stub out できない

というもの。そらそうか。

確認したバージョンは Sinon 2.3.2

今回試しに作ったコードでは

FetchUrlApp

を使っていたので、

class Sender {
  urlFetcher() {
    return (typeof FetchUrlApp == 'object') ? FetchUrlApp : {fetch: function(){}};
  }
}

として間接的に stub out 可能なオブジェクトを用意することとした。具体的に stub out するコードはこんな感じ。

var sender = new Sender();
sinon.stub(sender, 'urlFetcher').callsFake(function() {
  return {
    fetch: function(uri, params) {
      return params.payload;
    }
  }
});

何をしているかというと、FetchUrlApp.fetch(uri, params) で POST を行う部分を stub out している。

  1. urlFetcher() というメソッドで本物の FetchUrlApp かそれを stub out 可能な構造のオブジェクトを得る
  2. stub out 可能なオブジェクトとは stub out したいメソッドをすでに持っているオブジェクト。ここでは fetch() メソッドを利用する予定なので、Node.js 環境で実行した際には {fetch: function(){}} というオブジェクトを返すようにしている。

てな具合。慣れないとうへぇって感じだし、結局 production 環境で動くことの確認は手動で一度でも動かしてみないとダメなので、本当に全自動というわけにはいかないが、安定して回るようになれば Node.js だけで高速に開発サイクルを回せるので、なかなかよいと思う。

何より、テスト向けに出てきたツールの中に GAS 開発独自のものはないので、GAS 以外のノウハウが活かしやすい。これは大きい。全部見返してみても、セットアップが済んでしまえば、あとは import / export が使えないことに気をつける以外は特別な部分はほぼないはずだ。

これであとは staging と production を切り替えられるような仕組みを用意すれば、普通の Web アプリのように GitHub Workflow でだいたいの開発を回せるはずだ。

よしよし。

  1. これまでは underscoreGS とか使っていた。 

  2. Drive API を通じて独自にバージョン管理をし始めるとこういう時に何が起きてるか分からなくて困るのだが、落ち着いて build 済み JS を Script Editor にコピペしてみよう。 

More