Google Apps Script でも テスト がしたい! (Clasp + Typescript + Jest)

f:id:silverbirder180:20200201212044p:plain
Google Apps Script + Typescript + Jest

Google Apps Script(以下,GAS)でライブラリを公開しました。ライブラリを開発する際、テストのフィードバックサイクルを短くするため、Clasp + Typescript + Jest という技術スタックを選択しました。 その開発体験について共有しようと思います。特段変わったことはしていません。

Google Apps Scriptのテストってどうしてますか?

script.google.comにアクセスしてデバッグ実行って、しんどくないですか?

f:id:silverbirder180:20200201234221p:plain
Google Apps Script Debugging ...

  • ネットワーク越しでステップ実行するため、遅い
  • 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で予定されている時間(分)を取得
  • 重複している予定は、連続した予定とみなす
  • 指定の時間・単語は、計算対象外とみなす (ランチなど)
  • 誰がいつ休みなのか、終日イベントから取得

実際にサンプルコードがあるので、ご参考下さい。

github.com