Help us understand the problem. What is going on with this article?

VSCodeデバッガを使ってCloud FunctionsとFirestoreの連携テスト(TypeScript)

目標

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 inpm 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というファイルを作ります。

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.jsonjest項目など)の中で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.jstestMatchという項目を追加して下記のようにするといいでしょう。

jest.config.js
module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
  testMatch: ["**/firebase_test/*.test.[jt]s"],
};

その他Jestの設定項目のリファレンスはこちら↓です。

https://jestjs.io/docs/ja/configuration

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

launch.json
{
  "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

ログインしていたら全ての動作を許可、そうでなかったら全て不許可のガバガバルールです。あくまで動作確認用。

firestore.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であることを明確にしています。

functions/src/index.ts
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 });
  });

テストコード

テストケースを記述しています。この記事の本題ではないので流し読みでいいです。

firebase_test/firebase.test.ts
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.jsonDebug Jest TestspreLaunchTaskを追加して、デバッグ実行時にfunctions/src/index.tsがコンパイルされるようにします。

.vscode/launch.json
{
  "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
を選びます。

スクリーンショット 2020-05-01 18.15.14.png

すると.vscode/tasks.jsonが作成されます。中身はこうなってるはず。

.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という名前にしていますが、別になんでもいいです。

.vscode/launch.json
{
  "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.tsfunctions/src/index.tsの両方のbreakpointを機能させる方法はわかりませんでした。

.vscode/launch.jsonに作った2つの設定を両方実行すると

Starting inspector on 127.0.0.1:9229 failed: address already in use

というエラーが出てしまいます。かと言って.vscode/launch.json上でポート番号を変えると動かない…。

わかる方がいらっしゃいましたら是非教えて下さい!!!!

agajo
あんなに勉強して、親に高い予備校代も出してもらって東大に入り、卒業したのに、今では家と食事を親に頼りながら、年金と住民税を払うためにトイレ掃除をしている者です。
https://portal.oka-ryunoske.work/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした