Firebaseアプリ開発のテストについて考えてみる

Daiki Urata (@daio7nohe)

話すこと

  • きっかけ
  • Unit Testingについて考える
  • E2E Testingについて考える
  • まとめ

きっかけ

  • Firebaseめっちゃ便利、だけど実プロダクトでの採用が不安
  • Security Rulesとか少し間違えただけで、一気に脆弱性を生んでしまいそうで怖い

テストをしっかり書こう!

ユニットテストについて考える

Cloud Firestoreのテスト

  • マストで書く
  • Security Rules間違えて情報が漏れたら大変

合計1億件以上の個人情報がFirebaseの脆弱性によって公開状態に (Gigazineより)

「Firebaseの脆弱性」ではなく、開発者がSecurity Rulesをしっかり設定していなかったために起こった出来事だった模様

Console Simulator

  • コンソールから手軽にテスト
  • 試しながらSecurity Rulesを記述できる
  • 手動でのテスト

Local Emulator

  • ローカルにEmulatorをインストール必要あり
  • 現在はNode.js SDK (@firebase/testing)のみサポート
  • Jest/Mochaなどでテストを書いていく感じ
  • カバレッジレポートも出してくれる

わざわざテスト用Firebaseプロジェクトを用意せずにCIでテスト自動化できる !

:tada:

参考: Cloud Firestoreのrulesのテストを全てローカルエミュレータを使うように書き換えた話

Emulatorインストール、起動

$ firebase setup:emulators:firestore
$ firebase serve --only firestore

セットアップ

import * as firebase from "@firebase/testing";

firebase.initializeTestApp({
   projectId: "my-test-project",
   auth: { uid: "alice", email: "alice@example.com" }
 });

デモ

どんなものか触ってみたい方は

https://github.com/firebase/quickstart-nodejs

に公式サンプルがあります。

Cloud Functionsのテスト

Http Functionsの場合

exports.helloWorld = functions.https
    .onRequest((request, response) => {
      response.send("Hello from Firebase!");
    });

ブラウザで手動確認

$ firebase serve --only functions

Firestore Functionsの場合

exports.createUsername = functions.firestore
  .document("user/{userId}")
  .onCreate((snap, context) => {
    const newValue = snap.data();
    const username = newValue.email.split("@")[0];
    return snap.ref.set({ username }, { merge: true });
  });

Cloud Functions shellで手動確認

$ firebase functions:shell
firebase > createUsername({ email: "7noeh@example.com" })
'Successfully invoked function.'
firebase > info: User function triggered, starting execution
info: Execution took 1522 ms, user function completed successfully

テストコードは?

firebase-functions-test

  • config値のモック
  • non-HTTP functionsの呼び出しを行ってくれる
  • offline/onlineモードがある

config値のモック

// config値
{
  "someservice": {
    "key":"THE API KEY"
  }
}
// functionsのコード
const functions = require('firebase-functions');
const key = functions.config().someservice.key;
// config値のモック
const test = require("firebase-functions-test")();
test.mockConfig({ someservice: { key: '23wr42ewr34' }});

Firestore Functions (non-Http Functions)のテスト

const test = require("firebase-functions-test")();
const myFunctions = require('../index.js');
// Make snapshot
const snap = test.firestore.makeDocumentSnapshot({foo: 'bar'}, 'document/path');
// Call wrapped function with the snapshot
const wrapped = test.wrap(myFunctions.makeUppercase);
wrapped(snap);

Http Functionの例

exports.helloWorld = functions.https
    .onRequest((request, response) => {
      response.send("Hello from Firebase!");
    });

Http Functionのテストコード例

import myFunctions from "../functions/index";

// Http Functions Testing
describe("helloworld", () => {
  test("should return message", () => {
    const req = { query: { text: "input" } };
    const res = {
      send: result => {
        expect(result).toBe("Hello from Firebase!");
      }
    };

    myFunctions.helloWorld(req, res);
  });
});

Firestore Functionの例

exports.createUsername = functions.firestore
  .document("user/{userId}")
  .onCreate((snap, context) => {
    const newValue = snap.data();
    const username = newValue.email.split("@")[0];
    return snap.ref.set({ username }, { merge: true });
  });

Firestore Functionsのテストコード例

(offlineモード)

import myFunctions from "../functions/index";
import sinon from "sinon";
const fft = require("firebase-functions-test")();

// Firestore Functions Testing
describe("createUsername", () => {
  test("shoud create username from email", () => {
    const setStub = sinon.stub();
    const snap = {
      data: () => ({
        email: "7nohe@example.com"
      }),
      ref: {
        set: setStub
      }
    };
    setStub.withArgs({ username: "7nohe" }, { merge: true }).returns(true);
    const wrapped = fft.wrap(myFunctions.createUsername);
    expect(wrapped(snap, { params: { userId: "user-id-12345" } })).toBe(true);
  });
});

感想

  • firebase-functions-testで簡単にnon-Http Functionのテストが書けてよい
  • ただ、offlineだとstub書くのが面倒なので、テスト用プロジェクト用意してonlineモードもあり

E2Eテストについて考える

テスト用プロジェクトを作成して行う

  • 今の所、こうするしかないと思う

  • Firestore emulatorが使えるといい

  • DBリセットとかは gcloud beta firestore import コマンドでテストデータをインポートする?

Firebase Test Lab

https://firebase.google.com/docs/test-lab/

*Android/iOSのみ

まとめ

  • Firestore/Functionsのユニットテストだけはしっかりやっていくべき
  • 自動化環境も整ってきている
  • E2Eはローカルで完結するような手法があればいいかも
  • もっといい方法があれば教えて欲しいです

Thank you!