Google Apps Script でも テスト がしたい! (Clasp + Typescript + Jest)
Google Apps Script(以下,GAS)でライブラリを公開しました。ライブラリを開発する際、テストのフィードバックサイクルを短くするため、Clasp + Typescript + Jest
という技術スタックを選択しました。
その開発体験について共有しようと思います。特段変わったことはしていません。
Google Apps Scriptのテストってどうしてますか?
script.google.comにアクセスしてデバッグ実行って、しんどくないですか?
- ネットワーク越しでステップ実行するため、遅い
- G Suite系のサービスと連携すると、サービス側の調整(データ準備とか)が面倒
- デバッグ機能が貧弱
とてもストレスフルです。単純なGASなら別に良いんですが、少し複雑なGASを作ろうと思うと、問題に感じます。
ローカルで動かそう
GASをローカル環境で動かすことができる ClaspというコマンドラインツールがGoogleより公開されています。 github.com
また、ClaspはTypescriptをサポートしているため、型を中心としたコーディングが可能となりました。 www.npmjs.com
Typescriptを選択すると、Interface設計が容易になります。もちろん、.gs
ファイルでも同様の事は実現できると思います。
次に、Jestと呼ばれるテストツールを組み合わせることで、ローカル環境でテストが可能になります。 jestjs.io
ただ、単純にテストコードが書けません。 例えば、カレンダーイベントを取得するテストをコーディングするとき、次のようなスクリプトを書いたとします。
const calendar: Calendar = CalendarApp.getCalendarById('<your google calendar id>'); calendar.getEvents(new Date('2020-01-01'), new Date('2020-01-02')).forEach((calendarEvent: CalendarEvent)=> { console.log(calendarEvent.getTitle()); });
こう書いてしまうと、本当のカレンダーイベントを取りに行ってしまいます。テストであれば、そういった処理は避けたいところです。
そこで、CalendarApp
を偽物のオブジェクト、つまりMockオブジェクトに差し替えるため、依存性逆転の原則(dependency inversion principle)を適用します。
interface ICalendarApp { calendars?: Array<ICalendar>; getCalendarById(id: string): ICalendar; } interface ICalendar { calendarEvents?: Array<ICalendarEvent> getEvents(startTime: Date, endTime: Date): Array<ICalendarEvent>; } interface ICalendarEvent { title?: string, getTitle(): string; } class CalendarAppMock implements ICalendarApp { calendars?: Array<ICalendar>; getCalendarById(id: string): ICalendar { return this.calendars![0].calendar } } class CalendarAppImpl implements ICalendarApp { getCalendarById(id: string): ICalendar{ const calendar: ICalendar = CalendarApp.getCalendarById(id); return calendar; } }
このようなインターフェース・クラスを準備し、先程のコードを次のようにします。
const calendar: ICalendar = new CalendarAppMock().getCalendarById(); calendar.getEvents(new Date('2020-01-01'), new Date('2020-01-02')).forEach((calendarEvent: ICalendarEvent)=> { console.log(calendarEvent.getTitle()); });
結果、CalendarApp
の代わりにMockオブジェクトを差し込めるようになりました。ローカルテストが可能となります。
もちろん、プロダクトコードでは、CalendarAppMock
ではなく、 CalendarAppImpl
を使用すれば良いです。
Mockで差し替えるオブジェクトが増えると、InversifyJSのようなDIコンテナを検討してみると良いかもしれません。
github.com
こうすることで、Jestによるテストが動作するようになります。
実際に、開発・公開したライブラリでも十分にテストをすることができました。
www.npmjs.com
CaAT $ npm run test -- --coverage > jest "--coverage" PASS __tests__/utils/dateUtils.test.ts PASS __tests__/group/groupImpl.test.ts PASS __tests__/member/memberImpl.test.ts ---------------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ---------------------|---------|----------|---------|---------|------------------- All files | 98.43 | 97.62 | 96.67 | 98.37 | __tests__ | 100 | 100 | 100 | 100 | generator.ts | 100 | 100 | 100 | 100 | src/calendar | 93.1 | 100 | 92.31 | 92.59 | calendarAppImpl.ts | 60 | 100 | 50 | 60 | 6,7 calendarAppMock.ts | 100 | 100 | 100 | 100 | src/group | 100 | 100 | 100 | 100 | groupImpl.ts | 100 | 100 | 100 | 100 | src/member | 100 | 94.74 | 100 | 100 | memberImpl.ts | 100 | 94.74 | 100 | 100 | 38 src/utils | 100 | 100 | 100 | 100 | dateUtils.ts | 100 | 100 | 100 | 100 | ---------------------|---------|----------|---------|---------|------------------- Test Suites: 3 passed, 3 total Tests: 23 passed, 23 total Snapshots: 0 total Time: 2.826s, estimated 6s Ran all test suites.
ライブラリとして提供する機能のテストが、たったの約3秒で終わります。 ストレスフリーにローカル開発が可能となりました。
詳しくは、実際に作ったライブラリのソースコード(__tests__)を御覧ください。
終わりに
GASは、とても便利です。生産性が向上します。 サクッとAPIを構築できますし、G Suiteとの連携も(当たり前ですが)簡単です。
ただ、メンテナンス性が低いコードになると、陳腐化され誰も面倒が見れなくなります。 常にクリーンであり続けるためには、テストコードは必須です。 GASを運用する方々には、是非ともテストコードを検討下さい。
え、あ、ちょっとまって。ライブラリの紹介!
アジャイル開発で、かつ、Google Calendarで予定管理しているチームには是非とも使って頂きたいライブラリです。 github.com
CaAT is the Google Apps Script Library that Calculate the Assigned Time in Google Calendar.
このツールでできることは、次のとおりです。
- 指定期間における特定ユーザーのGoogle Calendarで予定されている時間(分)を取得
- 重複している予定は、連続した予定とみなす
- 指定の時間・単語は、計算対象外とみなす (ランチなど)
- 誰がいつ休みなのか、終日イベントから取得
実際にサンプルコードがあるので、ご参考下さい。