Cloud Firestoreの勘所 パート4 — 単体テスト
Cloud FunctionsのFirestoreトリガーの自動テスト🤖
パート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で書く場合に躓いた時の参考にもなるはずです。
テストの準備
まずは次のテストライブラリー郡をインストールします。
npm install --save-dev firebase-functions-test
npm install --save-dev mocha
npm install --save-dev @types/mocha
npm install --save-dev chai
npm install --save-dev @types/chai
firebase-functions-test
上で紹介した公式のFirebase単体テストライブラリーです。
mocha
JavaScriptのBDD(ビヘイビア駆動開発)テストフレームワークです。
Chai
Assertライブラリーです。power-assert などでも良いですが、公式サンプルがChaiを使っているので、それに従いました。
テスト環境の準備
テストファイルを追加
functions/src/test
に index.test.ts
を追加します。パート2で実行した watch
が走っていると、 functions/lib/test/index.test.js
が生成されて次のようなディレクトリー構造になります。
functions/lib
├── index.js
├── index.js.map
└── test
├── index.test.js
└── index.test.js.map
functions/src
├── index.ts
└── test
└── index.test.ts
packages.jsを設定
次に、packages.jsのscripts配下に、以下を追加します。
"test": "mocha --timeout 5000 --reporter spec lib/test/**/*.js"
npm run test
で上記コマンドが実行できるようになります。
オンラインモードのテストはネットワーク依存なのでたまに時間がかかることがあり、timeout
を 5000
(ms)と甘めにしています。
テストの失敗を確認
index.test.ts
に次のように記述しましょう。
import * as chai from 'chai';
const assert = chai.assert;describe('DESC', () => {
it('IT', () => {
assert.equal("foo", "bar");
});
});
chai
のimportでエラーが出る場合は次の設定を tslint.json
に追加すると直るはずです。
"no-implicit-dependencies": [true, "dev"]
functions
ディレクトリー上でnpm run test
を実行すると、次のように失敗するはずです。
次にassert
している行を次のように必ず成功するように書き換えてから再実行して 1 passing
で成功すればOKです✅
assert.equal("foo", "foo");
オンラインモードのテストの準備
テスト用プロジェクトの初期化
まず次のように必要なライブラリーのインポート・テスト用プロジェクトの初期化をします。
// assertを使えるように
import * as chai from 'chai';
const assert = chai.assert;
// Firebaseライブラリ
import * as admin from 'firebase-admin';
import * as fftest from 'firebase-functions-test';
// 自動テスト用プロジェクトを初期化
const test = fftest({
databaseURL: 'https://firestore-test-mono.firebaseio.com',
storageBucket: 'firestore-test-mono.appspot.com',
projectId: 'firestore-test-mono'
}, `${__dirname}/../../keys/serviceAccountKey.json`);
ここで大事なのは、「自動テスト用プロジェクト」を別途用意しておくことです。なぜなら、オンラインモードではテスト実行の度に実際にデータが生成されたり場合によってはクリーンアップ処理で全データを消したりするからです。公式ドキュメント のオンラインテストの説明にも「テスト専用の Firebase プロジェクトとやり取りするテスト」と明記されています。
本番環境と別に開発環境用など用意していても、さらに別に自動テスト用に用意するべきですし、複数人開発の場合は開発者ごとに用意するのが望ましいです。無料プランで済むので追加のコストにはならないはずです。感覚的には、開発者ごとにローカル環境を用意するのと似ています。ここでは firestore-test-mono
という新規プロジェクトを用意しています。
「自動テスト用プロジェクトを初期化」のために入れる値はFirebaseでWebアプリを追加するダイアログを表示するなどして取得します。
(すべて公開しても問題ない値なものの少し抵抗あって apiKey
は伏せてしまいました。)
第2引数で与えている `${__dirname}/../../keys/serviceAccountKey.json` の秘密鍵は、以下から生成できます。
serviceAccountKey.json
にリネームして functions/keys
に配置すると、`${__dirname}/../../keys/serviceAccountKey.json`でアクセスできます。このファイルは漏洩するととてもまずいので、Git管理している場合は .gitignore
に登録してコミット対象からは外して、ローカルでも大切に扱いましょう🔐
トリガーの記述されたファイルとFirestoreをインポート
次に、トリガーの記述されたファイルとFirestoreをインポートします。index.ts ファイル中に `admin.initializeApp(functions.config().firebase);`が書かれているので、 admin.firestore()
へアクセスできるのはそのインポートが済んだ後であり、順番に注意です(この順番を破ると実行時エラーになります)。
import * as target from '../index';
const firestore = admin.firestore();
ここで得られる firestore
オブジェクトの接続先は先ほど用意したテストプロジェクトとなっています。
これで、ようやく準備が整いましたので、トリガーのテストを書いていきます。
Firestoreトリガーのテスト
本体実装のおさらい
パート2 にも貼りましたが、本体実装は次のようになっています。
以下を実行している比較的シンプルな処理です。
- Cloud FunctionsのFirestoreトリガーで、
users/{userId}/posts
へのドキュメント追加・更新処理を監視 - 追加・更新処理がなされたら、ルートの
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
にて次のようにインポートして、
import * as myUtil from '../util';
単体テストごとに呼ばれるafterEach
あたりに以下を追加します。
afterEach(async () => {
await myUtil.deleteCollection(firestore.collection('posts'));
});
先ほどは onUsersPostCreate(新規追加のトリガー)のみでしたが、onUsersPostUpdate(更新のトリガー)も追加した完成形はこちらです:
Firebaseプロジェクトのリポジトリーもあります:
テストをデバッグするには?
Visual Studio Codeを使っている場合、launch.json
に次のように書いて、 F5
でデバッグ実行すると、任意のコード行に貼ったブレークポイントで止めてデバッグできます。
{
"version": "0.2.0",
"configurations": [{
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}/functions",
"name": "Mocha",
"program": "${workspaceRoot}/functions/node_modules/mocha/bin/_mocha",
"args": [
"${workspaceRoot}/functions/lib/test/**/*.js"
],
"sourceMaps": true,
"outFiles": ["${workspaceRoot}/functions/lib/test/**/*.js"],
"console": "integratedTerminal"
}
]
}
printデバッグよりもずっと効率良くデバッグできるので、活用していきましょう。
オンラインモードの自動テストの所感
firebase-functions-test が今月リリースされたばかりなこともあり、本記事を書くにあたって初めて触りました。少なくともFirestoreトリガーのテストに関してはかなり良い感じにささっと書ける印象を受けました。
気になる点は、ネットワークを介したテストなので、実行時間が多少かかることでしょうか。上記のようなテストでは、1テストあたり150ms〜500msくらいかかりました。これはオフラインテストと比べて明確に不利な点です。
これを踏まえると、以下くらいの塩梅が良い気がしています。
- オンラインテストは、1関数あたりに数をあまり書きすぎない(正常系とと異常系1つずつくらい?)
- 細かいテストはオフラインテストか、Firebaseに依存しない通常の単体テストで行う
パート5は未定ですが、気が向いたらここまでで触れられなかった細かいことを拾っていこうかなと思っています🧐