パート3まで理解すれば、もう充分Firestoreを活用してアプリを作れる状態になっていると思います。

ただ、ここまでまだ自動テストについて触れていなかったので、本記事ではパート2にてCloud FunctionsのFirestoreトリガーを使って実装したコピー処理の自動テストを書いていきます。

Cloud Functionsの自動テストは必要?

そもそも、Cloud Functionsの自動テストは必要かどうかですが、筆者は無くてもあまり問題ないと思っています。TypeScriptでキレイに書いていれば、慣れればデプロイして一発で普通に動くことが多く、またそのあとの変更時のデグレも型が守ってくれるためなかなか壊れることが無いと感じています。

そのため、特に自信の無いプレーンな関数(入出力のはっきりした関数)だけたまに限定的にテストを書く程度で済む気もしています。

とはいえ、デプロイするまで動作確認できないのは微妙ですし、やはりテストをしっかり書けばそれに応じてコードが堅牢になって安心感も高まっていきます。また、特に複数人開発では開発環境といえどもバグを含むコードをデプロイすると問題になりやすく、なるべくその前に手元でテストしておきたいものです。

というわけで筆者は「Cloud Functionsの自動テストは常に必ず必要というわけでは無いが、手間のかかりすぎない範囲である程度書いておくほうが良い」というスタンスです。

Cloud Functionsのテストの概要

公式ドキュメントによくまとまっています。

サンプルもこちらにあります:

2018年4月に firebase-functions-test としてリリースされたテストライブラリーが用いられています。

firebase-functions-test のおかげで、以前と比べてかなりテストが書きやすくなりました。

オフラインモードとオンラインモード

オフラインモードは、副作用のある操作をすべてスタブ化して行うテストです。スタブ化の手間がかかりますし、また副作用を伴う箇所でのミスは検知できません。Cloud Functionsから副作用を省くとかなりスカスカになってきて、それならロジックだけプレーンな関数に切り出してテストと大差無い気がしてしまいます🤔また、オフラインモードのテストに関してはすでに記事があることもあり、本記事では割愛します。

オンラインモードは、データベースへの書き込みなどが実際に行われ、副作用が発生した後の結果を検証する形となります。トリガーの起点から処理完了後の結果までをカバーできるテストとなるため、きちんと書かれたオンラインモードのテストが通ればデプロイ後も基本的にそのままきちんと動くとみなせます。

というわけで、本記事ではオンラインモードのテストをTypeScriptで書いていきます。公式ドキュメント・サンプルはJavaScriptなので、TypeScriptで書く場合に躓いた時の参考にもなるはずです。

テストの準備

まずは次のテストライブラリー郡をインストールします。

firebase-functions-test

上で紹介した公式のFirebase単体テストライブラリーです。

mocha

JavaScriptのBDD(ビヘイビア駆動開発)テストフレームワークです。

Chai

Assertライブラリーです。power-assert などでも良いですが、公式サンプルがChaiを使っているので、それに従いました。

テスト環境の準備

テストファイルを追加

functions/src/testindex.test.ts を追加します。パート2で実行した watch が走っていると、 functions/lib/test/index.test.js が生成されて次のようなディレクトリー構造になります。

packages.jsを設定

次に、packages.jsのscripts配下に、以下を追加します。

npm run test で上記コマンドが実行できるようになります。

オンラインモードのテストはネットワーク依存なのでたまに時間がかかることがあり、timeout5000 (ms)と甘めにしています。

テストの失敗を確認

index.test.ts に次のように記述しましょう。

chai のimportでエラーが出る場合は次の設定を tslint.json に追加すると直るはずです。

functions ディレクトリー上でnpm run test を実行すると、次のように失敗するはずです。

まずはテスト失敗を確認

次にassert している行を次のように必ず成功するように書き換えてから再実行して 1 passing で成功すればOKです✅

オンラインモードのテストの準備

テスト用プロジェクトの初期化

まず次のように必要なライブラリーのインポート・テスト用プロジェクトの初期化をします。

ここで大事なのは、「自動テスト用プロジェクト」を別途用意しておくことです。なぜなら、オンラインモードではテスト実行の度に実際にデータが生成されたり場合によってはクリーンアップ処理で全データを消したりするからです。公式ドキュメント のオンラインテストの説明にも「テスト専用の Firebase プロジェクトとやり取りするテスト」と明記されています。

本番環境と別に開発環境用など用意していても、さらに別に自動テスト用に用意するべきですし、複数人開発の場合は開発者ごとに用意するのが望ましいです。無料プランで済むので追加のコストにはならないはずです。感覚的には、開発者ごとにローカル環境を用意するのと似ています。ここでは firestore-test-mono という新規プロジェクトを用意しています。

「自動テスト用プロジェクトを初期化」のために入れる値はFirebaseでWebアプリを追加するダイアログを表示するなどして取得します。
(すべて公開しても問題ない値なものの少し抵抗あって apiKey は伏せてしまいました。)

各種設定値を取得

第2引数で与えている `${__dirname}/../../keys/serviceAccountKey.json` の秘密鍵は、以下から生成できます。

秘密鍵のJSONファイルを生成

serviceAccountKey.json にリネームして functions/keys に配置すると、`${__dirname}/../../keys/serviceAccountKey.json`でアクセスできます。このファイルは漏洩するととてもまずいので、Git管理している場合は .gitignore に登録してコミット対象からは外して、ローカルでも大切に扱いましょう🔐

トリガーの記述されたファイルとFirestoreをインポート

次に、トリガーの記述されたファイルとFirestoreをインポートします。index.ts ファイル中に `admin.initializeApp(functions.config().firebase);`が書かれているので、 admin.firestore() へアクセスできるのはそのインポートが済んだ後であり、順番に注意です(この順番を破ると実行時エラーになります)。

ここで得られる firestore オブジェクトの接続先は先ほど用意したテストプロジェクトとなっています。

これで、ようやく準備が整いましたので、トリガーのテストを書いていきます。

Firestoreトリガーのテスト

本体実装のおさらい

パート2 にも貼りましたが、本体実装は次のようになっています。

以下を実行している比較的シンプルな処理です。

  1. Cloud FunctionsのFirestoreトリガーで、users/{userId}/posts へのドキュメント追加・更新処理を監視
  2. 追加・更新処理がなされたら、ルートの posts にコピー

onUsersPostCreate関数のテストを記述

次のように書けます。

test.firestore.makeDocumentSnapshot などが目新しいですが、 DocumentSnaphost オブジェクトをサクッと作れるテスト用メソッドです。

before にて、 wrap した関数に makeDocumentSnapshot で生成したデータを与えて関数を実行します。

it では、テスト用プロジェクトのFirestoreルートの posts にコピーされた実際のドキュメントを取得して、正しくコピーされているか検証しています。

少し上に戻って、 after では cleanUp メソッドという後片付けメソッドを記述して、全体テスト終了後に必ず呼ばれるようにしておきます。

Firestoreルートの posts にコピーされたドキュメントの削除

上のコードでは、コピーされたドキュメントがそのまま残ってしまって、今後のテストに影響を及ぼしてしまうので、その後片付けも必要です。個別ドキュメントを消すのもありですが、 posts コレクション全てを消す方が確実です。

Firestoreのコレクションを一気に消すメソッドは用意されていないので、次のような関数を util.ts として用意します。

ちなみに、この実装は公式ドキュメントを参考にしつつTypeScript・async/awaitで書き直したものです。

index.test.ts にて次のようにインポートして、

単体テストごとに呼ばれるafterEach あたりに以下を追加します。

先ほどは onUsersPostCreate(新規追加のトリガー)のみでしたが、onUsersPostUpdate(更新のトリガー)も追加した完成形はこちらです:

Firebaseプロジェクトのリポジトリーもあります:

テストをデバッグするには?

Visual Studio Codeを使っている場合、launch.json に次のように書いて、 F5 でデバッグ実行すると、任意のコード行に貼ったブレークポイントで止めてデバッグできます。

テストをデバッグ実行した様子

printデバッグよりもずっと効率良くデバッグできるので、活用していきましょう。

オンラインモードの自動テストの所感

firebase-functions-test が今月リリースされたばかりなこともあり、本記事を書くにあたって初めて触りました。少なくともFirestoreトリガーのテストに関してはかなり良い感じにささっと書ける印象を受けました。

気になる点は、ネットワークを介したテストなので、実行時間が多少かかることでしょうか。上記のようなテストでは、1テストあたり150ms〜500msくらいかかりました。これはオフラインテストと比べて明確に不利な点です。

これを踏まえると、以下くらいの塩梅が良い気がしています。

  • オンラインテストは、1関数あたりに数をあまり書きすぎない(正常系とと異常系1つずつくらい?)
  • 細かいテストはオフラインテストか、Firebaseに依存しない通常の単体テストで行う

パート5は未定ですが、気が向いたらここまでで触れられなかった細かいことを拾っていこうかなと思っています🧐

--

--