自分は Google Apps Script(以下GAS)大好き人間なのですが、以下2点の不満があります。
- ブラウザでエディタを開いて作業しなければいけない。
- ローカルでのテストが書けない。
テストについてはいくつかGAS対応のフレームワークがあるようですが、どれも既存のプロジェクトに組み込んで使用するようです。
- how to unit test google apps scripts? - Stack Overflow
- zixia/gast: Google Apps Script TAP Testing-framework
- Google Code Archive - Long-term storage for Google Code Project Hosting.
Slack用勤怠管理Botの「みやもとさん」では QUnit
を使っているようです。
- masuidrive/miyamoto: Google Apps Scriptで書かれたSlack用勤怠管理Botの「みやもとさん」
- GoogleAppsScriptでユニットテスト - Qiita
- QUnit for Google Apps Script でユニットテストを行う手順 – oki2a24
でも既存プロジェクトに組み込む形だと結局ブラウザでエディタを開いて書くことになるんですよね。。
そんな不満を解決してくれるかと思って以下の記事を試してみましたが、、
残念ながら自分が一番やりたいことができなかったです。
それは 「UrlFetchApp などを使ってるコードのテストはどうやるの?」 ということ。
GASには UrlFetchApp
の他にも MailApp
や SpreadsheetApp
など多数のオリジナルな便利クラスが用意されています。
最悪、他はできなくても UrlFetchApp
を使ったコードのテストだけでもできれば、APIで取得した内容をSlackに投げるとかの処理でテストが書けます。
何かいい方法はないかなーと探していると素晴らしいツールを見つけました。
gas-local でモックを作って解決
gas-local
はGASのモック作成ライブラリです。
Github の説明には「Execute and test」と書かれていますがテストの機能は含まれておらず、mocha
などと一緒に使う必要があります。
UrlFetchApp
だけでなく、モックを用意すれば MailApp
など他のクラスでも対応可能です。
Readme にあるのは MailApp
のモックを作るやり方だけなので、実際に UrlFetchApp
のモックを作ってテストを書いてみます。
初期設定
初期設定については主に前述の記事にある通りです。
$ npm install -g node-google-apps-script
$ gapps auth ./client_secret_<client_secret_key>.json
Please visit the following url in your browser (you will only have to do this once): https://accounts.google...
-> URLを踏んで認証
Successfully Authenticated with Google Drive!
$ mkdir gas-test
$ cd gas-test
$ gapps init <Project ID>
# Example
# gapps init jiofe3g4jHfr5dekD5deNIO9e...
$ tree .
.
├── gapps.config.json
└── src
└── コード.js
テストの準備
テスト対象のコードを用意します。
以下のコードを少し改変して使用します。
function myFunction(url) {
if( url == null ) url = 'http://www.yahoo.co.jp';
var response = UrlFetchApp.fetch(url);
var myRegexp = /<title>([\s\S]*?)<\/title>/i;
var match = myRegexp.exec(response);
var title = match[1];
title = title.replace(/(^\s+)|(\s+$)/g, '');
Logger.log(title);
return title;
}
次はテストに必要なライブラリを追加していきます。
yarn
を使うと package.json
の scripts
フィールドに色々と書かなくて済むので捗ります。
$ npm install -g yarn
$ yarn add gulp-mocha gas-local sync-request
gas-local
は前述の通り、GASのモック作成ライブラリです。
sync-request
は同期的にリクエスト処理を実行してくれるライブラリです。
Node.js では基本的に非同期処理でリクエストが実行されるるので、UrlFetchApp
と同じく同期的にリクエスト処理を行うにはこのライブラリが必要となってきます。
テストコード
準備ができたらテストコードを用意します。
$ mkdir test
$ touch test/Code-test.js
var assert = require('assert');
var gas = require('gas-local');
var request = require('sync-request');
// ローカルでのテストでは Logger クラスを無効化する
gas.globalMockDefault.Logger.enabled = false;
var defMock = gas.globalMockDefault;
var customMock = {
// UrlFetchApp クラスのモックを作成
UrlFetchApp: {
// fetch 関数と同じ動きをする処理を書いていく
fetch: function(url) {
var req = request('GET', url);
return req.body.toString();
}
}, __proto__: defMock
};
// ソースフォルダの指定はプロジェクトルートからの相対パス
var glib = gas.require('./src', customMock);
describe('コード.js', function() {
// mocha がタイムアウトするまでの時間を延長(default:2000ms)
this.timeout(5000);
it('myFunction for yahoo', function() {
assert.equal(glib.myFunction(), 'Yahoo! JAPAN');
});
});
モックの作り方はコードを見るとなんとなくわかると思います。
UrlFetchApp.fetch
関数を用意し、それと同じ動きをする処理を sync-resquest
を利用して作成します。
また、Logger.log
での出力処理はローカルテスト時には必要ないので無効化しておき、
サイトによってはアクセスに時間がかかって mocha
の実行がタイムアウトしてしまうので時間を延長しておきます。
あと注意点として、 ソースフォルダの指定はプロジェクトルートからの相対パス で行います。
test
フォルダ内に用意したテストコードに処理を書いているので、../src
と書きたくなるところですが、それだとエラーになります。
テスト実行
準備ができたらテストを実行します。
$ yarn mocha
yarn mocha v0.21.3
$ "/path/to/code/gas-test/node_modules/.bin/mocha"
コード.js
✓ myFunction for yahoo (1290ms)
1 passing (1s)
✨ Done in 1.97s.
おおー!うまくいきました。
※たまに以下のようなポップアップが表示されます。
「許可」を選択するか少し待つと自動的に消えます。
別のサイトも試してみましょう。
〜略〜
it('myFunction for yahoo', function() {
assert.equal(glib.myFunction(), 'Yahoo! JAPAN');
});
// 追加
it('myFunction for livedoor', function() {
assert.equal(glib.myFunction('http://www.livedoor.com/'), 'livedoor');
});
});
$ yarn mocha
yarn mocha v0.21.3
$ "/path/to/code/gas-test/node_modules/.bin/mocha"
コード.js
✓ myFunction for yahoo (1232ms)
✓ myFunction for livedoor (244ms)
2 passing (1s)
✨ Done in 2.25s.
素晴らしい…!
テストが通ったらアップする
ここまでできればあとは gulp
を用いれば簡単です。
gapps
コマンドを使いたいので、タスク内でコマンドの実行ができる gulp-exec
を追加します。
gulp-load-plugins
も追加していますがそちらは好みでどうぞ。
$ yarn add gulp-exec gulp-load-plugins
$ touch gulpfile.js
var gulp = require("gulp");
var $ = require("gulp-load-plugins")();
gulp.task("mocha", function() {
return gulp
.src(["test/*.js"])
.pipe($.mocha({ reporter: "spec" }));
});
gulp.task("upload", ["mocha"], function() {
return gulp.src(".")
.pipe($.exec("gapps upload"));
});
gulp.task("watch", function() {
return gulp
.watch(["src/**", "test/**"], ["upload"]);
});
それぞれを実行
$ yarn gulp mocha
# mocha でのテストだけを実行
$ yarn gulp upload
# mocha でのテストを実行し、エラーがなければアップロードを実行
$ yarn gulp watch
# ファイルを監視して変更があったら upload タスクを実行
わりと簡単にできてしまいました…!
ブラウザでアップされたファイルを開いて実行してみましょう。
[17-04-XX 19:26:08:208 PDT] Yahoo! JAPAN
バッチリですね!
URL を livedoor に書き換えてもきちんと動きます。
シフトJIS サイトに対応する
シフトJIS サイトとして有名?な阿部寛のサイトを追加してテストしてみます。
〜略〜
it('myFunction for livedoor', function() {
assert.equal(glib.myFunction('http://www.livedoor.com/'), 'livedoor');
});
// 追加
it('myFunction for Shift_JIS', function() {
assert.equal(glib.myFunction('http://abehiroshi.la.coocan.jp/top.htm'), '阿部 寛のホームページ');
});
});
$ yarn mocha
yarn mocha v0.21.3
$ "/path/to/code/gas-test/node_modules/.bin/mocha"
コード.js
✓ myFunction for Yahoo! (1145ms)
✓ myFunction for livedoor (308ms)
1) myFunction for Shift_JIS
2 passing (2s)
1 failing
1) コード.js myFunction for Shift_JIS:
AssertionError: '���� ���̃z�[���y�[�W' == '阿部 寛のホームページ'
+ expected - actual
-���� ���̃z�[���y�[�W
+阿部 寛のホームページ
at Context.<anonymous> (test/Code-test.js:38:14)
error Command failed with exit code 1.
エラーになりました。
文字コードを指定してない場合は UTF-8
となるので当然ですね。
さてここから修正していくのですが、GASならでの問題を解決していく必要があります。
まず、GASでは UrlFetchApp.fetch
から返ってきたオブジェクトに対して getContentText
関数を用いて文字コードを指定します。
モックを作成する処理では取得した内容をテキストとして返しているので、当然ながら getContentText
がなくてエラーになります。
かと言って getContentText
での文字コード指定を書かないと、ローカルでのテストは通っても Web での実行はエラーになります。
この問題を手っ取り早く解決するには、UrlFetchApp.fetch
から返ってきたオブジェクトを調査して getContentText
関数があれば実行、なければ何もしないという処理にすると良いです。
次に、モックとして作成した UrlFetchApp.fetch
にどうやって指定する文字コードを渡すかという問題があります。
前述のように UrlFetchApp.fetch
には文字コードを指定するオプションはなく、返ってきたオブジェクトに対して文字コードを指定します。
Web の方でエラーが出ないように本来は指定する必要のない情報を渡すにはどうすればいいか試行錯誤した結果、contentType
の charset
で指定するのが良さそうなことがわかりました。
※この点についてはもっと適切な方法がある気がするので詳しい方はコメントください。
渡されたオプションから charset
を取得する際は Node.js の標準ライブラリである querystring
を使い、指定した文字コードへの変換には iconv-lite
を使うことにします。
$ yarn add iconv-lite
- JavaScriptで関数の存在有無を見分けるには - shohu33's diary
- PHP でレスポンスヘッダーの Content-Type に文字コードを指定する方法 | Webセキュリティの小部屋
- Query String | Node.js v7.8.0 Documentation
- node.js - nodejs encoding using request - Stack Overflow
- Node.jsでデータの文字コードを変換する - 情報アイランド
- ashtuchkin/iconv-lite: Convert character encodings in pure javascript.
以上の点をふまえて修正したコードが以下になります。
// 引数に char を追加
function myFunction(url, char) {
if( url == null ) url = 'http://www.yahoo.co.jp';
if( char == null ) char = 'utf8';
// GASの contentType のデフォルトは application/x-www-form-urlencoded
var params = {
'contentType': 'application/x-www-form-urlencoded; charset='+char
};
var response = UrlFetchApp.fetch(url, params);
// getContentText 関数の存在チェック
if( typeof(response.getContentText) === 'function' )
response = response.getContentText(char);
var myRegexp = /<title>([\s\S]*?)<\/title>/i;
var match = myRegexp.exec(response);
var title = match[1];
title = title.replace(/(^\s+)|(\s+$)/g, "");
Logger.log(title);
return title;
}
var assert = require('assert');
var gas = require('gas-local');
var request = require('sync-request');
var querystring = require('querystring'); // 追加
var iconv = require('iconv-lite'); // 追加
gas.globalMockDefault.Logger.enabled = false;
var defMock = gas.globalMockDefault;
var customMock = {
UrlFetchApp: {
// 文字コード指定に対応するよう修正
fetch: function(url, params) {
var req = request('GET', url);
var char = params.contentType.replace(/\s/g, '');
char = querystring.parse(char, ';');
req = iconv.decode(req.body, char.charset);
return req.toString();
}
}, __proto__: defMock
};
〜略〜
it('myFunction for Shift_JIS', function() {
// 文字コードを引数で指定
assert.equal(glib.myFunction('http://abehiroshi.la.coocan.jp/top.htm', 'sjis'), '阿部 寛のホームページ');
});
});
テスト(とアップロード)を実行します。
$ yarn gulp upload
yarn gulp v0.21.3
$ "/path/to/code/gas-test/node_modules/.bin/gulp" upload
[17:39:22] Using gulpfile /path/to/code/gas-test/gulpfile.js
[17:39:22] Starting 'mocha'...
コード.js
✓ myFunction for yahoo (2435ms)
✓ myFunction for livedoor (319ms)
✓ myFunction for Shift_JIS (311ms)
3 passing (3s)
[17:39:25] Finished 'mocha' after 3.73 s
[17:39:25] Starting 'upload'...
[17:39:33] Finished 'upload' after 7.48 s
✨ Done in 15.65s.
無事にテストが通ってアップされました!
[17-04-XX 19:26:08:208 PDT] Yahoo! JAPAN
Web の方でもエラーなく実行されます。
URL を阿部寛のサイトに書き換えても、シフトJISを指定すればきちんと動作します。
まとめ
既存のコードに多少手を加える必要はありますが、GAS の独自関数のテストが書けるようになりました。
gas-local すっごーい!
そしてありがとう。
今回のコードは Github に置いておくので、動作を確認したい方などはご自由にどうぞ。