firebase emulator + Python


Firebase emulator がリリースされていて　とても開発がはかどりました。
まだ情報がすくないのでPythonからの利用方法をまとめます。

## Firebase emulator 

firestore をメインのDBとして使用しているので、単体テストで色々なテストデータを登録して作成するのに、ローカルでemulator が動かせると便利です。

下記のような仕組みが準備されているので、まずローカルにemulatorをInstallする

https://firebase.google.com/docs/emulator-suite/install_and_configure?hl=ja

開発言語自体はPythonを使いたいのですが、emulatorは　node, javaで動いているので事前にインストールしておく必要があります。

```shell
% node --version
v16.13.2
% npm install -g firebase-tools

% firebase login
Already logged in as {$USER}
```

* firebaseのプロジェクトを選択時に下記のようなエラーがでることがあります

    ```
    % firebase projects:list
    ✖ Preparing the list of your Firebase projects

    Error: Failed to list Firebase projects. See firebase-debug.log for more info.
    ```

    表示されているとおりfirebase-debug.log を開いてみると下記のError表示

    ```text
    Error: HTTP Error: 401, Request had invalid authentication credentials. Expected OAuth 2 access token
    ```

    login できているって表示されてたのに、なぜ！！と思いながら上記のエラーメッセージでググると下記のパラメータで強制的に再認証シーケンスを走らせることで対応できるとのことです。
    ```shell
    % firebase login --reauth --no-localhost
    ```

* emulatorの設定
```shell
firebase init emulators
```
* emulator実行

```shell
% cd firebase_emulator 
% firebase emulators:start
i  emulators: Starting emulators: firestore, storage
i  firestore: Firestore Emulator logging to firestore-debug.log
i  ui: Emulator UI logging to ui-debug.log

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌───────────┬────────────────┬─────────────────────────────────┐
│ Emulator  │ Host:Port      │ View in Emulator UI             │
├───────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ localhost:8090 │ http://localhost:4000/firestore │
├───────────┼────────────────┼─────────────────────────────────┤
│ Storage   │ localhost:9199 │ http://localhost:4000/storage   │
└───────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

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

* Firebase 単体テスト: https://firebase.google.com/docs/rules/unit-tests?hl=ja#rut-v2-common-methods
* vscode のPython開発設定情報参考１：https://atmarkit.itmedia.co.jp/ait/articles/2107/16/news029.html
* vscode のpython開発設定情報参考2 : https://dorapon2000.hatenablog.com/entry/2020/04/29/152251



## python からemulatorに接続する

以下のように OSの環境変数にlocal emulator に設定することで
通常通りfirebase admin sdk を使用すると　アクセス先がローカルのfirebase emulator になる

* この情報が公式手順上明記されておらず最初はできないのかとおもいました。
    * まだemulator 自体が、βバージョンということなので今後のドキュメント含めた拡張に期待します
* 注意点として本番のfirestoreを誤って上書きしてしまわにように、テストのときのProject,ルートドキュメントを別の名前にしておいたほうが良いと思います。
* Localや、GCP上でインスタンスが生成されたままだとfirebase_admin.initialize_appで既にあるとエラーになります。下記のように_appsのlenを調べて初期化必要かどうかを判定しています

```python
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore

os.environ["FIRESTORE_EMULATOR_HOST"]="localhost:8090"
os.environ["GCLOUD_PROJECT"]="my_project"

cred = credentials.Certificate(self.config['firebase']['certjson'])
if (not len(firebase_admin._apps)):
    firebase_admin.initialize_app(cred)
self.db = firestore.client()

```

## pytest

例えば、pytest上で、emulator をつかって、テスト条件を一定にするために最初にdocument, collection をクリアにするというようなコードは以下のように書くことができます。（emulatorでも普通のfirestoreの使い方と変わりません。）

``` python
    def setup_method(self,method):
        print('method={}'.format(method.__name__))
        delete_collection(self.db.collection(u'hoge').document(u'fuga1').collection(u'foo'),16)
        delete_collection(self.db.collection(u'hoge').document(u'fuga2').collection(u'baa'),16)
        doc_ref = self.db.collection(u'hoge').document(u'fuga1')
        doc_ref.set({
            u'key1': False,
            u'key2': True
        })
        doc_ref = self.db.collection(u'hoge').document(u'fuga2')
        doc_ref.set({
            u'key1' : False,
            u'key2' : True
        })
```


* Test例：
以下は、ab, cd というデータが事前に登録されていた場合正しく収集できるかという関数のテスト例です。

``` python
    def test_idea_collection(self):
        col_ref = self.db.collection(u'hoge').document(u'fuga1').collection(u'foo')
        timestr1 = '2022/2/3 16:48:11'
        time1 = datetime.strptime(timestr1 + '+0900', '%Y/%m/%d %H:%M:%S%z')
        timestr2 = '2022/2/6 16:48:11'
        time2 = datetime.strptime(timestr2 + '+0900', '%Y/%m/%d %H:%M:%S%z')
        col_ref.add({
            u'author': 'x-man',
            u'content':'ab',
            u'createdAt':timestr1
        })
        col_ref.add({
            u'author': u'x-woman',
            u'content':'cd',
            u'createdAt':timestr1
        })
        collectresult,lastupdate = self.main_target.collect_foo('fuga1')
        assert collectresult.count('ab') == 1
        assert collectresult.count('cd') == 1
        assert collectresult.count('ef') == 0
```

* test 実行例
    * emulator, pytestがローカルでInstallされていれば以下のようにテスト実行を確認できます

```shell
% pytest test_hogehoge.py 
========================================================================= test session starts =========================================================================
platform darwin -- Python 3.9.0, pytest-7.0.0, pluggy-1.0.0
rootdir: /Users/hoge/src
collected 13 items                                                                                                                                                    

test_hogehoge.py .............                                                                                                                        [100%]

========================================================================= 13 passed in 20.66s =========================================================================
```

### test 実装時の参考
* Test setup, tear down
  - Test全体のSetup,Teardown、　TestごとのSetupをどう書くかが効率的で安定したテストを書く肝になる。
  - https://udemyfun.com/python-pytest-setup-teardown/

* vscode でのpytest 実行方法
  - https://gri.jp/media/entry/358