npm
TypeScript
Firebase
cloudfunctions
Firestore

Cloud Functions for Firebase を TDD で開発する

Cloud Functions for Firebase のデプロイ、遅いですよね。
早い時は 30 秒くらいで終わるのですが、遅い時はなぜか10分以上かかったりするし、1行だけ変えて実行結果を見たい時に数分待たされるとイライラします。

できればローカルで開発して正常に動くことが確認できてから deploy したいです。
ローカルでの関数の実行  |  Firebase で関数のエミュレートができますが、コマンドラインで JSON の準備するのは大変だし TDD で開発ができるかというとそうではありません。

ということで、 Cloud Functions をローカルでテストできるようにしてみました。 (まだ Cloud Firestore トリガーしかテストできません)

starhoshi/rescue-fire: A test helper for Cloud Functions.

どうやるの

Cloud Firestore トリガーで動く Cloud Functions は、 event という変数から全て始まります。

exports.updateUser = functions.firestore.document('users/{userId}')
  .onCreate(event => {
    console.log('old name', event.data.data().name)
    return event.data.ref.update({name: 'new name'})
})

この event を作り出せてしまえば Admin SDK を使い Cloud Functions とほぼ同等のコードを動かせるわけです。
rescue-fire はこの event を作ってくれます。

TDD で Cloud Functions を開発してみる

rescue-fire を使い TDD での開発をしてみましょう。このサンプルでは TypeScript を使っています。

1. インストール

yarn add --dev rescue-fire typescript

2. 設定ファイル JSON の取得

サーバーに Firebase Admin SDK を追加する  |  Firebase を見て、JSON をダウンロードしてください。

重要: このファイルには、サービス アカウントの秘密暗号鍵などの機密情報が含まれます。秘密鍵の情報は機密扱いとし、公開レポジトリには保存しないでください。

3. テストライブラリ

ここでは jest を使っていますが、好きなものを使って良いです。

yarn add --dev jest ts-jest

3. テストを書く

User が作成されたら name を update する、という関数を作ることにしました。

関数の中身はこのようになりますね。

const changeName = (event: functions.Event<DeltaDocumentSnapshot>) => {
  console.log('old name', event.data.data().name)
  return event.data.ref.update({ name: 'new name' })
}

このコードをテストすると、以下のようになります。

import 'jest'
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
import * as Rescue from 'rescue-fire'

// Admin SDK を使うための初期セットアップ
beforeAll(() => {
  const serviceAccount = require('./your-firebase-adminsdk.json')
  admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
  })
})

test('update name', async () => {
  // テストデータの準備
  const data = {name: 'name'}
  const user = await admin.firestore().collection('user').add(data)
  const event = Rescue.event(user, data) // イベントを作成

  // Cloud Functions を発火
  await changeName(event)

  // アップデートされたユーザを取得し、 name が変更されていることを確認
  const updatedUser = await admin.firestore().collection('user').doc(user.id).get()
  expect(updatedUser.data()!.name).toBe('new name')
})

わかりやすくするために先に実装を書いたため厳密には TDD ではありませんが、ローカルで Cloud Functions を擬似的に動かしテストを書くことができました。

これは小さな関数ですが、 orderable.test.ts ではもっと巨大な関数が rescue-fire を使いテストされています。

Optional 引数

event.params, event.data.previous.data() などを必要に応じて引数で設定できるようにしています。
どういう風に渡せるかは index.d.ts を参考にしてください。

4. 最後に deploy

では最後に deploy しましょう。先ほど作った changeName(event) を関数に渡してあげるだけです。

exports.updateUser = functions.firestore
  .document('users/{userId}')
  .onCreate(event => {
    return changeName(event)
})

注意事項

完全に event をエミュレートしているわけではないので、一部動作に問題が発生するかもしません。

すでに Cloud Functions を deploy 済みの状態だと Cloud Functions が発火とローカルのテストが同時に動くことになるので、テスト前に Cloud Functions を消しておく必要があります。 (これはなんとかしたい)

また、 Firestore への書き込みを Mock しているわけではないので実行時間が長い & 予期せぬエラーが発生してしまうかもしれません。しかし、下手に Mock するよりそのまま叩いた方がテストとしては良いと思っています。

おわり

rescue-fire を使うことでローカルで Cloud Functions のテストを書きながら開発できるようになりました。
Cloud Functions を快適に開発していきましょう :muscle:

starhoshi/rescue-fire: A test helper for Cloud Functions.