📲

Firebase Emulatorで実現するローカル検証環境の構築とLuupでの応用

2024/11/02に公開

はじめに

Software Development部 サーバーチームのt-kurimuraです。こちらの記事は、LuupのCM放映に合わせた「少し早いAdvent Calendar」の2日目の記事です。

Luupのソフトウェア開発では、FirebaseをBaaSとして利用しています。そのメリットの1つとして、Firebase Emulatorの充実した機能が挙げられます。今回は、Firebase Emulatorの概要と、Luupでの具体的な活用方法についてご紹介します。

Firebase Local Emulator Suite とは

Firebase

そもそもFirebaseとは、Google Cloud を基盤とするマネージド インフラストラクチャを、アプリの迅速な開発と成長に役立つよう提供されている Google のモバイル開発プラットフォームです

2016年頃から話題になりはじめ、いまなお根強い人気を誇ります

非常に様々な機能がありますが、主によく利用されるのは

  • データーベース: Firestore, etc..
  • フロントエンドサーバー: Hosting
  • バックエンドサーバー: Cloud Functions
  • 認証基盤: Firebase Authentication

(他多数)

などの機能群です

Firebase Local Emulator Suite

Firebase Local Emulator Suiteは、Firebaseが提供するさまざまなサービス(Firestore、Authentication、Functionsなど)をローカル環境でシミュレーションできるツール群です。これにより、実際のプロダクション環境に影響を与えることなく、ローカルで開発やテストを行うことができます。

https://firebase.google.com/docs/emulator-suite#what-is-emulator-suite

エミュレーターの起動とUI

上記のように様々な機能が連携して動くFirebaseですが、FirebaseのEmulatorは、その多くのサービスをローカルでエミュレートできます

セットアップに従ってFirebaseを設定したあと、以下のコマンドでローカル環境に各サービスが立ち上がります

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

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ 127.0.0.1:5002 │ n/a                             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Pub/Sub        │ 127.0.0.1:8085 │ n/a                             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Storage        │ 127.0.0.1:9199 │ http://127.0.0.1:4000/storage   │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at 127.0.0.1:4400
  Other reserved ports: 4500, 9150

また、Emulator SuiteのWeb UIにアクセスすることで、各サービスのステータスを確認できます。

エミュレーターの気の利くところ

ここまでは一般的な情報をお伝えしましたが、ここからはLuupでFirebaseエミュレーターを活用し始めて分かったポイントをご紹介します。

クライアントアプリからもFirebaseに接続できる

WebアプリケーションやiOS・Androidといったネイティブアプリからも、エミュレーターを介してFirebaseにアクセスすることが可能です。

例えば、AndroidアプリでFirestoreのインスタンスを初期化する際に、エミュレーターのIPアドレスとポートを指定することで接続できます。

class EmulatorSuite {

    fun emulatorSettings() {
        // [START fs_emulator_connect]
        // 10.0.2.2 is the special IP address to connect to the 'localhost' of
        // the host computer from an Android emulator.
        val firestore = Firebase.firestore
        firestore.useEmulator("10.0.2.2", 8080)

        firestore.firestoreSettings = firestoreSettings {
            isPersistenceEnabled = false
        }
        // [END fs_emulator_connect]
    }
}

(firebase/snippets-android)

ローカルホスト以外の端末からローカルネットワークで接続する場合は、firebase.jsonの"host"設定を変更する必要があるため注意が必要です。

firebase.json

{
  "functions": {
    "name": "functions",
    "host": "localhost", // <- ここをエミュレーターが起動している端末のローカルIPに
    "port": 5001
  },
}

電話番号(SMS)認証のエミュレートが可能

Firebase Emulatorは、Firebase Authenticationのエミュレーションもサポートしています。先ほどの方法でモバイル端末からエミュレーターに接続し、LUUPへのログインを試すと、エミュレーターのログに次のようなメッセージが表示されます。

i  To verify the phone number {入力した電話番号}, use the code XXXXXXX.

このコードをアプリ側で入力することでログインが可能です。

また、認証コードはエミュレーターをCurlで呼び出して取得することもできます。

% curl -X GET \
  "http://<FIREBASE_EMULATOR_HOST>:9099/emulator/v1/projects/luup-afe4f/verificationCodes" | \
  jq '.verificationCodes[] | select(.phoneNumber == "+81<PHONE_NUMBER>") | .code'
% {入力した電話番号}

これによって後述の自動テストの際にもテストコード上でエミュレーターから認証番号を取得でき、認証を成功させることができます。

HTTP以外のトリガー関数の実行も簡単

Firebase Cloud Functionsでは、FirestoreやStorage、PubSubなどのイベントをトリガーにした関数を実行できます。また、Cloud Schedulersで定期的に実行されるCron処理的な関数も利用されていますが、こうした処理は通常トリガーを手動で作成する必要があります。

Firebase EmulatorにはCloud Schedulersは含まれておらず、自動的には実行されませんが、Cloud Functionsのshellを利用して、Admin SDK内のコマンドをインタラクティブに実行することが可能です。

firebase functions:shell

✔  functions: Emulator started at http://localhost:5000
i  functions: Loaded functions: helloWorld, helloOnCall
firebase > 

例えば、以下のようなスコアリセット関数を定義している場合:

exports.resetScore = functions.pubsub.schedule("every 24 hours").onRun(async (context) => {
  try {
    const scoresRef = admin.firestore().collection("scores");
    const snapshot = await scoresRef.get();
    const batch = admin.firestore().batch();
    snapshot.forEach(doc => {
      batch.update(doc.ref, { score: 0 });
    });
    await batch.commit();
    console.log("Scores have been reset successfully.");
  } catch (error) {
    console.error("Error resetting scores:", error);
  }
});

Cloud Functions shellを使用してこの関数を手動で実行することができます。

> cronApi.resetScore()
i  functions: Loaded environment variables from .env, .env.luup-afe4f.
'Successfully invoked function.'

なお、Shell内でタイムアウトを設定し、仮想的なCronを実行するなかなかトリッキーなワークアラウンドも存在します。

LuupでのFirebase Emulatorの活用

実機テストの自動化

Luupでは、QAチーム内で自動テストを実行しています。XCTestとFirebase Test Labを活用することで、ライドなどの重要な導線のテストを半自動で実行しています。Firebase Test Labはクラウド上の仮想デバイスでアプリケーションを実行・操作でき、従来人間が手動で行っていた操作の担保が可能です。

Firebase Test Labを活用し、仮想端末でのテストを自動化することで、手動のQAは新機能や複雑な機能の検証に集中しやすくなっています。

バックエンドとフロンエンドの統合開発の簡易化

Luupでは、クライアント機能とバックエンドを統合し、特にWebフロントエンドとバックエンドをモノレポとして管理しています。また、モバイルアプリやバックエンドがそれぞれFirestoreにアクセスする構造のため、クライアントとバックエンドが足並みを揃えながら同時に開発されるケースが多くあります。

クライアント開発者がバックエンドも開発することも少なくありませんが、開発人数が増えてくると、リモートのステージング環境で開発がバッティングすることがあります。Firebase Emulatorを使用することで、各開発者が自分の手元で専用のバックエンド環境を構築でき、チーム間の調整やCloudFunctionsで課題になりがちなデプロイの待ち時間を回避することが可能になります。

DockerやDocker Composeなどを用いてローカル環境を構築するのと比べて、Firebaseはインフラ管理の手間が不要なため、モバイルアプリなどの開発者にもバックエンドを扱いやすい環境となっています。

一方で、Firestore以外のデータベースや外部サービスを利用する場合、Firebase Emulatorだけでは環境が整いません。今後、Docker Composeを導入することで、より多様なサービスを導入しやすくする計画も進めています。

インテグレーションテスト

Luupでは特に車両との連携処理を中心に、データベースとAPIを統合して検証するインテグレーションテストを実施しています。これらのテストは主にIoTチームによって実装されていますが、エミュレーターを伴う処理が多いため、実行時間が長くなる傾向があります。

Luupでは、Pull Requestごとに約180のケースのインテグレーションテストが実行されますが、GitHub Actions上でJobを分割し、それぞれのJobがエミュレーターを起動し並列実行することで、テストは約5分で完了します。

一方で、プロダクションコードの設計によってはユニットテストが書きにくい構造になっている部分があり、そのためインテグレーションテストに頼っている部分もあります。今後、メンテナンス性向上のため、プロダクション側のドメインモデルを見直し、チームでの改善アプローチを計画しています。

おわりに

今回は、Firebase Emulatorを活用したLuupの検証環境の整備についてご紹介しました。Firebase Emulatorにより、ローカル環境でのテストが手軽になり、クライアントアプリとの統合や、バックエンド・フロントエンドの同時開発が一段とスムーズになりました。また、QAチームが自動テストを駆使することで、より複雑な検証にも集中できる環境が整いつつあります。

今後もLuupのサーバーチームやソフトウェア部では、さらなる開発者体験効率向上を行いながら、LUUPがよりよい体験になることを目指しています。これを通して、私たちが目指す「街じゅうを『駅前化』するインフラ」を支えるプロダクト開発を、プロダクト志向かつ未来志向で進めていきます。

Luupのミッションに共感し、技術的な挑戦を楽しみながら一緒に新たなインフラを作りたいエンジニアの方はぜひお待ちしております。
https://recruit.luup.sc/

Luup Developers Blog

Discussion