見出し画像

Firebase - Local Emulator Suiteを使ったテストのすすめ

こんにちは!

Firebaseを使った開発していますか? 最近はFirebaseを使ったアーキテクチャを結構多く採用しています。

従来の三層のアーキテクチャと異なる部分が多くとてもおもしろいです。あまり複雑なものでなければほとんどFirebaseでかなり楽して出来てしまうのでないかと思います。

Firebaseを使った開発の場合、データベースに対する読み書きがフロントエンドで完結してしまうので、フロントエンドの開発でほとんど終わってしまうことが多いと思います。単純なCRUDのためにAPIをわざわざ作る必要がなくなりとても楽だと思います。

ただし、それでもバックエンドにやらせたい仕事は残ります。例えば: 

• 非同期でなにか処理をやらせたい場合。
• シークレットを扱う処理をするような場合。
• Firestoreに不得意な処理をやらせる場合。(検索、トランザクション・・・等)

です。

この様な場合はFirebase Functionsを使う事がおすすめです。Firebase Authenticationと連携してくれたりFirestoreをトリガーとして使えたりするので、シンプルに実装することが出来ます。

Firebase Functionsはイベントに反応して動きます。HTTPリクエスト、フロントからのコール、データベース上の変更、ユーザーのサインアップ、CRON等・・・といったイベントに対応しています。

さて、Firebaseでなにからなにまで提供してくれるで、開発をするのは楽なのですが、一方で、サーバレスと同じテスト環境をローカルに整えるのが従来の開発よりも複雑です。

ということでFirebaseのテストについて今日はご紹介しようと思います。(前置き長い。。。)

Firebaseでどの様なテストをすべきか?

Firebaseでテストすべき事は主に以下になるかと思います。

• FirestoreのSecurity Ruleが正しく設定されているか?
• 実装したモジュールが正しく動くか?
• 外部に公開されたFunctionsが正しく動くか?

Security Ruleのテストはクライアントから直接DBに書きに行くFirebaseならではですね。Security Ruleのテストはここでわかりやすく書かれています。今回は下の2つについてサンプルコードをご紹介致します。

Firebase Local Emulator Suite

FirestoreやCloud FunctionsをローカルでテストさせるためにLocal Emulator Suiteというツールが提供されているので、入れておく必要があります。またそれ以外にもテストランナーや諸々のツールを入れておきます。Dev Dependenciesはこんな感じです。(Typescriptを使っています。)

// package.json
{  
  "devDependencies": {
   "@firebase/testing": "^0.20.11",
   "@types/chai": "^4.2.12",
   "@types/chai-as-promised": "^7.1.3",
   "@types/mocha": "^8.0.3",
   "chai": "^4.2.0",
   "chai-as-promised": "^7.1.1",
   "firebase-functions-test": "^0.2.0",
   "firebase-tools": "^8.9.2",
   "mocha": "^8.1.3",
   "ts-node": "^9.0.0",
   "tslint": "^5.12.0",
   "typescript": "^3.8.0"
  }
}

Emulatorを動かすには以下のような設定が必要です。

// firebase.json
{
 "firestore": {
   "rules": "firestore.rules",
   "indexes": "firestore.indexes.json"
 },
 "functions": {
   "predeploy": [
     "npm --prefix \"$RESOURCE_DIR\" run lint",
     "npm --prefix \"$RESOURCE_DIR\" run build"
   ]
 },
 "storage": {
   "rules": "storage.rules"
 },
 "emulators": {
   "functions": {
     "port": 5001
   },
   "firestore": {
     "port": 8080
   },
   "hosting": {
     "port": 5000
   },
   "pubsub": {
     "port": 8085
   },
   "ui": {
     "enabled": true
   }
 }
}

これで firebase emulators:start を実行するとローカルでエミュレータが動き始めます。こんな感じです。

> $ firebase emulators:start                                                                                                                                                                                                                               ⬡ 10.21.0ISS-1 ✓]
i  emulators: Starting emulators: functions, firestore, hosting, pubsub
⚠  hosting: The hosting emulator is configured but there is no hosting configuration. Have you run firebase init hosting?
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, hosting
✔  functions: Using node@10 from host.
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  hosting: The hosting emulator is configured but there is no hosting configuration. Have you run firebase init hosting?
i  pubsub: Pub/Sub Emulator logging to pubsub-debug.log
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "/Users/yusuke/myproject/functions" for Cloud Functions...

┌───────────────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! View status and logs at http://localhost:4000 │
└───────────────────────────────────────────────────────────────────────┘

┌───────────┬──────────────────────────────────┬─────────────────────────────────┐
│ EmulatorHost:PortView in Emulator UI             │
├───────────┼──────────────────────────────────┼─────────────────────────────────┤
│ Functions │ localhost:5001                   │ http://localhost:4000/functions │
├───────────┼──────────────────────────────────┼─────────────────────────────────┤
│ Firestore │ localhost:8080                   │ http://localhost:4000/firestore │
├───────────┼──────────────────────────────────┼─────────────────────────────────┤
│ HostingFailed to initialize (see above) │                                 │
├───────────┼──────────────────────────────────┼─────────────────────────────────┤
│ Pub/Sub   │ localhost:8085                   │ n/a                             │
└───────────┴──────────────────────────────────┴─────────────────────────────────┘
 Other reserved ports: 4400, 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

モジュールをテストする

それでは、Firestoreになんらかの書き込みをするモジュール Utilをテストしてみましょう。ポイントはFIRESTORE_EMULATOR_HOSTを書き換えてクラウドではなくローカルマシンを指定させることです。ポート番号はFirestoreのエミュレータからとってください。今回は8080です。

それが出来てしまえば、その後は普通にモジュールをexpect等を使ってテストしていくことが出来ます。insertRecordを実行した後にFirestoreに書き込まれているかどうかをテストします。

// util.test.ts
process.env.GCLOUD_PROJECT = 'MY_PROJECT'
process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080'

import {describe} from 'mocha'
import * as chai from 'chai'
import * as chaiAsPromised from 'chai-as-promised'

const expect = chai.expect
chai.use(chaiAsPromised)

import * as admin from 'firebase-admin'
admin.initializeApp()
const db = admin.firestore()

describe('Util', () => {
 it('should write document successfully', async () => {
   const util = new Util()
   const documentId = 'documentid'
      
   const result = await util.insertRecord(documentId)

   const snapshot = db.doc(`/records/${documentId}`).get()
   expect(snapshot.exists).to.equal(true) 
 })
})

HTTPをテストする

それでは次にhttpで公開されているfunction helloWorldをテストしてみましょう。認証されていない場合は失敗する事をテストし、認証されている場合は 'hello world'が返ってくることをテストします。

ここのポイントはfunctionを firebase-functions-test でラップするところです。ラップされたfunctionには任意でデータや、コンテキストを与えてテスト出来ます。コンテキストのオブジェクトを操作すれば認証状態も簡単に再現することが出来ます。

import * as fft from 'firebase-functions-test'
const test = fft()
import { helloWorld } from '../src/index'

describe('helloWorld', () => {
 it('should reject unauthenticated request', async () => {
   const wrapped = test.wrap(helloWorld)
   const data = {}
   const context = {} // <- 認証されていない
   return await expect(wrapped(data, context)).to.be.rejected
 })
 
 it('should return string for authenticated request', async () => {
   const wrapped = test.wrap(helloWorld)
   const data = {}
    const context = { // <- 認証されている
     auth: {
       uid: 'test'
     }
    }
  return await expect(wrapped(data, context)).to.eventually.equal('hello world')
 }) 
})


これでテストをかきながら開発をすることが出来ます。ちなみにCIをする時には以下のコマンドで一発でエミュレータの立ち上げとテストの実行を行う事が出来ます。

firebase emulators:exec 'npm run test'

今回の場合は npm run test でテストを実行するようにしていますが、適宜テストスクリプトに変更してください。

まとめ 

いかがでしたでしょうか? Firebaseはサーバレス製品なので、ローカルに同じ環境を作るの少し面倒ですが、Emulator Suiteの登場によって本物のFirebase製品にかなり忠実なエミュレータを使ったテストを自分のマシンで行うことが出来ます。これを利用することによってローカルで高速にテストしながら開発をして、開発が終わったらクラウドにデプロイするといったことができるようになります。

最後まで読んでいただきありがとうございます。もしよかったらスキお願いしますー。

この記事が気に入ったらサポートをしてみませんか?