目標
VSCodeのデバッガを使って、breakpointを打ってデバッグしながら下記2種類のテストを行う。
- Firestoreのセキュリティルールのテスト
- Cloud Functionsの、Firestoreをトリガーとする関数の動作テスト
コードはすべてTypeScriptで書く。
各種バージョン
- firebase-tools 8.1.1
- VSCode 1.44.2
- npm 6.14.4
- node 13.8.0
- jest 25.4.0
- ts-jest 25.4.0
nodeのバージョンはCloud Functionsのサーバーで使用されるものに合わせて8.15.0
などにしてもいいかもしれません。その際はnodenv
などを使いましょう
ここではnodeバージョンのズレは気にせずいくことにします。
↓Cloud Functionsのnodeのバージョンについて↓(リンク先は英語。日本語版もありますが、日本語版は更新が遅いことがあるので)
https://firebase.google.com/docs/functions/manage-functions#set_nodejs_version
https://cloud.google.com/functions/docs/concepts/exec#runtimes
前提
Firebase CLI がインストール済みで、
Firebaseプロジェクトは作成済みで、
firebase init
なども済んでいるとします。
この記事では、その状態から、テストをどう実行していくかを説明していきます。
テストツールはJestを使います。
なお、この記事内で言う「Cloud Functions」は全て「Cloud Functions for Firebase」を指します。
#準備
ts-jestの公式リファレンスを参考にしていきます。
https://github.com/kulshekhar/ts-jest
https://kulshekhar.github.io/ts-jest/user/install
必要なパッケージのインストール
プロジェクトディレクトリ内で、下記コマンドを実行します。
npm init -y
npm i -D jest @types/jest typescript ts-jest
npm init
で、package.json
を作成します。npm
に関する設定がここに書き込まれます。
その際いろいろ聞かれることがありますが、-y
オプションをつけるとすべてyes
と答えたことになり、聞かれなくなります。
npm i
はnpm install
の略です。-D
または--save-dev
をつけると「これは開発時に使うパッケージであり、プロダクションには含めない」という意味になります。
プロダクション側でtypescriptを使っている場合は、これだけ-Dを外してインストールしてください。既にインストールされているなら不要。
functions/
にまとめてインストールすることで容量節約?
こちらのCode Lab(https://google.dev/pathways/firebase-emulators )では、テストに必要なパッケージもfunctions
用のパッケージも全てfunctions/
内のnode_modules/
にインストールしています。
その方がnode_modules/
が一つですみ、容量が100〜200MBほど節約できます。
が、しかし、それはやっぱり構造上おかしい気がするので、今回はプロジェクトディレクトリ自体にもnode_modules/
を持ち、functions/node_modules/
とは分けることにします。
用が済んだらそれぞれのnode_modules/
は消してもいいです。npm i
のコマンドでいつでも復活できます。
(そもそもnode_modules/
重すぎるだろ…)
Jestの設定
Jest自体の設定はいくつかの書き方ができます。
私が参考にした記事では引き続きpackage.json
内に"jest"
という項目を作って書き込んでいるものが多かったですが、
今回はts-jestがうまくやってくれる仕組みがあるのでそれを利用します。
jest.config.js
の作成
こちらのコマンドを実行します。
npx ts-jest config:init
するとjest.config.js
というファイルが作成されます。
jestの動作確認
この段階で動作確認してみましょう。
プロジェクトディレクトリにfirebase_test/
というディレクトリを作り、hoge.test.ts
というファイルを作ります。
// TODO: このファイルは動作確認が済んだら消す
describe("check if jest works", () => {
test("typescript works", async () => {
let x: number = 1;
let y: number = 2;
let result: boolean = true;
expect(x < y).toBe(result);
});
});
型指定を使うことで、このままではJavaScriptとして解釈不可能なコードにしてみました。TypeScriptが動作していることを確認するためです。
テストコードを書いたら、次のコマンドを実行します。
npx jest
下記のような出力が出ればOK
PASS firebase_test/hoge.test.ts
check if jest works
✓ typescript works (2ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.704s, estimated 2s
Ran all test suites.
(optional)テストファイルの指定
Jestが探すテストコードはjest.config.js
(またはpackage.json
のjest
項目など)の中でtestMatch
という項目でglobを使って指定できます。(正規表現ではないので注意!)
何も指定しなかった場合のデフォルトはこうです。
**/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?(x)
読み取りにくいですが、/firebase_test/hoge.test.ts
というパスはこれにマッチするので、見つけてもらえます。**.test.ts
という名前なら大丈夫です。
例えばもしfirebase_test/
以下にある**.test.ts
または**.test.js
というファイルを対象にしたい場合は、jest.config.js
にtestMatch
という項目を追加して下記のようにするといいでしょう。
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/firebase_test/*.test.[jt]s"],
};
その他Jest
の設定項目のリファレンスはこちら↓です。
VSCodeの設定
次は、今のテストコードを、VSCodeからブレイクポイントを打ちながら実行できるようにします。
.vscode/launch.json
を作成し改造
.vscode/launch.json
を作成します。
VSCodeの左のバーからデバッグボタン(再生ボタンと虫が重なったマーク)を押し、上部の歯車ボタンを押せば作成できます。
次に、下記リンク先に従って、launch.json
の中身を次のようにします。デフォルトの中身は消して構いません(必要な設定がある人は残してくださいね)。
https://jestjs.io/docs/ja/troubleshooting.html#vs-code%E3%81%A7%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E3%81%99%E3%82%8B
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/jest",
"--runInBand"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
}
]
}
実行
先程作ったfirebase_test/hoge.test.ts
に適当にブレイクポイントを打って、
VSCodeのデバッグメニューから、今作った設定Debug Jest Tests
を選び、実行します。
ブレイクポイントで一時停止しながらテストが実行され、結果が出力されればOKです!
一時停止時の変数の中身は、左部のClosure
タブに隠れてます。
Firebaseエミュレータでテスト
それではいよいよ、Firebaseエミュレーターを使っていきます。準備は終わって本番です。
必要なパッケージをインストール
まずはfirebaseのテストに必要なパッケージをインストールします。
npm i -D @firebase/testing
なお、参考にする記事によってはfirebase setup:emulators:firestore
という工程がある場合がありますが、2020年5月1日時点での最新バージョン(firebase-tools 8.1.1)では不要なはずです。(参考: https://firebase.google.com/docs/rules/emulator-setup )
テストに関係するソースコード
Firestore Security Rules
ログインしていたら全ての動作を許可、そうでなかったら全て不許可のガバガバルールです。あくまで動作確認用。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
Cloud Functions
items
コレクションに何かが追加されたら、yeah:"yeah"
というフィールドを追加します。
型指定をわざわざ書いて、TypeScriptであることを明確にしています。
import * as functions from "firebase-functions";
export const addYeah = functions.firestore
.document("items/{itemId}")
.onCreate((snapshot, context) => {
let x: string = "yeah";
let flag: boolean = true;
snapshot.ref.set({ yeah: x }, { merge: flag });
});
テストコード
テストケースを記述しています。この記事の本題ではないので流し読みでいいです。
const fs = require("fs");
import * as firebase from "@firebase/testing";
// セキュリティルールに関するテストグループ
describe("security rule tests", () => {
beforeEach(async () => {
// 毎テスト前に、セキュリティルールを読み込みます
await firebase.loadFirestoreRules({
// functionsと連携しない場合、projectIdは何でもいい
projectId: "my-test-project",
rules: fs.readFileSync("firestore.rules", "utf8"),
});
});
afterEach(async () => {
// 毎テスト後に、テストで使用したクライアントアプリを削除します。
await Promise.all(firebase.apps().map((app) => app.delete()));
});
test("can not get if not logged in", async () => {
const db: firebase.firestore.Firestore = firebase
.initializeTestApp({
projectId: "my-test-project",
})
.firestore();
const doc = db.collection("subjects").doc("math");
await firebase.assertFails(doc.get());
});
test("can get if logged in", async () => {
const db: firebase.firestore.Firestore = firebase
.initializeTestApp({
projectId: "my-test-project",
auth: { uid: "test_user", email: "test_user@example.com" },
})
.firestore();
const doc = db.collection("subjects").doc("english");
await firebase.assertSucceeds(doc.get());
});
});
// functionsに関するテストグループ
describe("functions tests", () => {
beforeEach(async () => {
// 毎テスト前に、セキュリティルールを読み込みます
await firebase.loadFirestoreRules({
// functionsと連携する場合、実際のprojectIdを入れる
projectId: "step-by-step-4c04c",
rules: fs.readFileSync("firestore.rules", "utf8"),
});
// 毎テスト前に、データベースを空にします。
await firebase.clearFirestoreData({ projectId: "step-by-step-4c04c" });
});
afterEach(async () => {
// 毎テスト後に、テストで使用したクライアントアプリを削除します。
await Promise.all(firebase.apps().map((app) => app.delete()));
});
test("add yeah field", async (done) => {
const db: firebase.firestore.Firestore = firebase
.initializeTestApp({
projectId: "step-by-step-4c04c",
auth: { uid: "test_user", email: "test_user@example.com" },
})
.firestore();
const doc = db.collection("items").doc("foobar");
let result1: firebase.firestore.DocumentSnapshot = await doc.get();
// はじめは該当データが存在しないことを確認
expect(result1.exists).toBe(false);
doc.set({ fuga: "fuga" });
doc.onSnapshot((snapshot) => {
let data2: firebase.firestore.DocumentData = snapshot.data();
console.log("data changed!!");
console.log(data2);
// functionsが正常に動作したら、done()を実行してテストを完了する
if (data2["yeah"] && data2["yeah"] === "yeah") {
done();
}
});
});
});
テストコードのデバッグの場合
breakpointを打つ
firebase_test/firebase.test.ts
上で適当にbreakpointを打ちましょう。
preLaunchTaskを設定し、自動でfunctions/src/index.ts
をコンパイル
functions
のエミュレータを起動する前に、TypeScriptで書いたfunctions/src/index.ts
をコンパイルする必要があります。
手動で、cd functions/
npm run build
cd ../
としても良いです。functions/package.json
に、このコマンドでtsc
が走ってコンパイルされるように設定されています。
しかし、毎回やるのは面倒なので自動化しましょう。
こちら↓の記事などを参考に、
https://qiita.com/TsuyoshiUshio@github/items/9879ea04cdd606982a65
.vscode/launch.json
のDebug Jest Tests
にpreLaunchTask
を追加して、デバッグ実行時にfunctions/src/index.ts
がコンパイルされるようにします。
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/jest",
"--runInBand"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229, ← このコンマと
"preLaunchTask": "npm: build - functions" ←この行を追加
}
]
}
これで、Debug Jest Tests
を実行すると毎回まず先にnpm: build - functions
とかいう名前のtask
が呼ばれます。
でもそんなものはまだ存在しないので作成しましょう。
手動で作ってもいいですし、VSCodeに大半を任せてもいいです。
任せる場合は、コマンドパレットを開いてtask
と入力し、
Tasks: Configure Default Build Task
npm: build - functions
を選びます。
すると.vscode/tasks.json
が作成されます。中身はこうなってるはず。
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build",
"path": "functions/",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [],
"label": "npm: build - functions",
"detail": "tsc"
}
]
}
ざっくり言うと「functions/
に移動してnpm run build
を実行する」という内容になっています。
label
の部分が、このタスクの名前になります。先程.vscode/launch.json
から指定したnpm: build - functions
になっていますね。もし名前が違ったら、合わせておきましょう。
エミュレータの実行
はじめてエミュレータを実行する場合のみ、エミュレータを実行する前に、
cd functions/
npm run build
を実行するか、
先程作ったDebug Jest Tests
を実行して、TypeScriptのコンパイルを実行しておきます。
でないとfunctions/lib/index.js
が存在しないため、エラーになってしまいます。
二回目以降はこのファイルが存在するので問題ないはずです。コンパイルによってこのファイルが更新された場合はエミュレータに自動で反映されます。
そうしたら、次のコマンドで、エミュレータを実行します。
firebase emulators:start
デバッグ実行!
VSCodeの左部のバーからデバッグを選択し、Debug Jest Tests
を実行します。
先程打ったbreakpointでテストの実行が一時停止するはずです!
これで、テストコードのデバッグができますね!
functions/src/index.ts
のデバッグの場合
breakpointを打つ
functions/src/index.ts
上で適当にbreakpointを打ちましょう。
.vscode/launch.json
に項目を追加
次にこちら↓の記事を参考に、/.vscode/launch.json
に次の設定を追加します
https://qiita.com/u4da3/items/9c2d13eee43c9652e7d1
Attach
という名前にしていますが、別になんでもいいです。
{
"version": "0.2.0",
"configurations": [
...
{
"name": "Attach",
"type": "node",
"request": "attach",
"processId": "${command:PickProcess}",
"outFiles": ["${workspaceRoot}/functions/lib/**/*.js"]
}
]
}
エミュレータの実行
そしてエミュレータを、次のオプションを付けて起動し直します。
firebase emulators:start --inspect-functions
これにより、エミュレータ上で動作するfunctions
がbreakpointを受け付けるようになります。
デバッグ実行!
そしたら、VSCodeのデバッグメニューから、今作った「Attach」を選んで実行します。
するとエミュレータを起動しているコンソールに
Debugger attached.
と表示されます。
この状態で、別のコンソールから次のコマンドを実行します。
VSCodeではターミナルウインドウを複数同時に開けるのでうまく使いましょう。
npx jest
としてテストコードを起動すると、functions/src/index.ts
上のbreakpointで止まります。
これをすると、テストコードの方はタイムアウトしますがそれでいいです。
これでfunctions
もデバッグできますね!
残る仕事
一回の実行でfirebase_test/hoge.test.ts
とfunctions/src/index.ts
の両方のbreakpointを機能させる方法はわかりませんでした。
.vscode/launch.json
に作った2つの設定を両方実行すると
Starting inspector on 127.0.0.1:9229 failed: address already in use
というエラーが出てしまいます。かと言って.vscode/launch.json
上でポート番号を変えると動かない…。
わかる方がいらっしゃいましたら是非教えて下さい!!!!