Expo+Firebaseのクライアント側テストについて


FirebaseのAuthentication, Firestore, Cloud Functionsを使用したExpoアプリにテストを導入する際の手法を模索してみました。ひとまず途中経過として投稿します。
結合テストに関してはDetoxを使用したかったのですが、現在のバージョンのExpoでは使用できないようなので、単体テストのみの記事になります。

テストツールはJestを使用します。
テストの際に実際のDBへのアクセスや認証を行う訳にはいかないので、ひとまずFirebaseのモック化のライブラリであるfirebase-mockを使ってみます。公式ライブラリではなく個人開発(最終コミットが1年以上前)のものなので、不十分なところが多いというのが正直な所感です。

firebase-mockのインストール

$ yarn add --dev firebase-mock

ちょっとトリッキーなコードですがこちらを参考にしました。

firebase.js
import firebasemock from "firebase-mock";
import MockFirestoreQuery from "firebase-mock/src/firestore-query";

const mockdatabase = new firebasemock.MockFirebase();
const mockauth = new firebasemock.MockFirebase();
const mocksdk = new firebasemock.MockFirebaseSdk(
  path => (path ? mockdatabase.child(path) : mockdatabase),
  () => mockauth
);

export default mocksdk;

テストコード上でimportし通常のFirebaseSDKと同様に使用できます。

import firebase from './firebase';

const auth = firebase.auth();
const db = firebase.firestore();

実行環境ごとに使い分ける

プロダクトコードでは通常のFirebaseSDKを使用し、テストコードではfirebase-mockを使用するように切り替える仕組みを作りたいと思います。
やり方は色々あると思いますが、ここでは例としてプロダクトコードのバンドルに影響が無いようにするためscriptでファイルを切り替える方法をとります。

js/firebase
  ├── _mock.js
  └── _product.js

firebaseディレクトリを作成し、先ほどのfirebase-mockをexportしたファイルと、同様にFirebaseSDKをexportしたファイルを入れます。

$ yarn add --dev cp-cli

cp-cliをインストールしscriptsを修正します。

package.json
  "scripts": {
    "start": "cp-cli js/firebase/_product.js js/firebase/index.js && expo start",
    "android": "cp-cli js/firebase/_product.js js/firebase/index.js && expo start --android",
    "ios": "cp-cli js/firebase/_product.js js/firebase/index.js && expo start --ios",
    "web": "cp-cli js/firebase/_product.js js/firebase/index.js && expo start --web",
    "eject": "expo eject",
    "prettier": "prettier --write 'js/**/*.js'",
    "test": "cp-cli js/firebase/_mock.js js/firebase/index.js && jest --coverage"
  },

これで、テスト時は_mock.jsがindex.jsに、アプリ実行時は_product.jsがindex.jsとしてディレクトリにコピーされるので、

js/firebase
  ├── _mock.js
  ├── _product.js
  └── index.js
import firebase from './firebase';

どちらの実行時にでもこのようにimportしておけばOKです。
また、.gitignorejs/firebase/index.jsを追加しコミットしないようにします。

Firebaseを使ったコードをテストしてみる

Redux/redux-thunkを想定し、簡単にFirebase Authenticationでサインインするアクションをテストしてみます。

authActions.js
import firebase from '../firebase';

const auth = firebase.auth();

export const signIn = (email, password) => async (dispatch) => {
  dispatch({
    type: "SIGN_IN"
  });
  try {
    const result = await auth.signInWithEmailAndPassword(email, password);
    dispatch({
      type: "SIGN_IN/SUCCESS",
      payload: result
    });
  } catch (error) {
    dispatch({
      type: "SIGN_IN/FAIL",
      error: true,
      payload: error
    });
  }
};
authAction.test.js
import firebase from "../firebase";
import * as actions from "./authActions.js";

const auth = firebase.auth();
auth.autoFlush();

describe("auth actions", () => {
  it('signIn', async () => {
    const credentials = {
      email: "[email protected]",
      password: "examplePassword"
    };
    auth.createUser(credentials);
    const dispatch = jest.fn();
    await actions.signIn(credentials.email, credentials.password)(dispatch);
    expect(dispatch.mock.calls.length).toBe(2);
    expect(dispatch.mock.calls[0][0].type).toBe("SIGN_IN");
    expect(dispatch.mock.calls[1][0].type).toBe("SIGN_IN/SUCCESS");
  });
});
$ yarn test
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

firebase-mockに独自のメソッドがいくつか用意されているので、それを使用して認証情報を作成しておいてからログイン処理を実行させます。
最初にautoFlush()を呼んでおくことででauthやfirestoreの状態が自動的に更新されるようです。(と、書いていますが挙動がよくわからなかったりします...)
認証情報の作成→loginを実行し、計2つのアクションがdispatchされるのを確認しています。

Firestoreも同じように使用できるのですが、一旦割愛します。

そもそもFirebase処理の単体テストは必要なのかどうか

書いていてなんですが、単体テストにおいてはFirebaseの処理をテストする必要のあるケースが少ないのではないかと気づきました。
現在開発中のExpoアプリでは、

  • ActionCreatorは最小限の薄い関数にする
  • FirebaseSDKのどのメソッドを呼ぶという情報のみActionに持たせる
  • それをMiddlewareで実行する

というようなRedux構成にし、単体テスト実行時には認証もDBの操作も実行しない(Actionの比較のみ)ようにしています。redux-api-middlewareに近い感じですね。