Firebase EmulatorをGitHub Actionsで動かしてUnitTestを行う手法について
始めに
この記事では Firebase Emulator を GitHub Actions 上で動かしつつ、Xcode 上で UnitTest を回す際に出た問題と、その解決法について紹介します。
前置き
現在、自分がゆるりと開発をしている firebase-ios-sdk のラッパーEasyFirebaseSwift
では、GitHub Actions を用いて UnitTest のチェックを行っています。
例えば以下のような Firestore にデータが保存できたという事実をテストするケースがあります。
func test_create_async() async throws {
let newModel = TestModel(message: "Async Test")
let ref = try await client.create(newModel)
let fetched: TestModel = try await client.get(uid: ref.documentID)
XCTAssertEqual(newModel.message, fetched.message)
}
このような場合をテストするには「Firestore のモッククラスを作る」「通信をインターセプトする」「ローカル用の Firestore 環境を用意する」と大きく 3 つの方法があると思います。
「モッククラスを作る」のは Firestore のような大規模なクラスだと口数が多く、他にも Auth や Storage にもテストをしたい場合はそれぞれにモックを用意する必要があると考えるとめちゃくちゃ大変だと思います。
また、「通信をインターセプトする」方法は、Swift だとURLProtocol
を使うと思いますがこちらの手法はあまり理解ができておらず、理論的にできそうという程度なので割愛します。
最後に「ローカル用の Firestore 環境を用意する」については公式がFirebase Emulatorという Firebase のローカル環境を用意しています。今回はこの Firebase Emulator を用いて UnitTest を行う前提で話を進めます。
また、Firebase Emulator は Firestore 以外に 以下のサービス にも対応しています。
- Auth
- Realtime Database
- Storage
- Functions
- Hosting
どんな問題が起きたのか
GitHub Actions 上で Firebase Emulator のセットアップが完了する前に Xcode 側で Firestore のユニットテストをしてしまうケースが発生しました。
なぜ発生したのか
Firebase Emulator は実行するとサーバーとして起動し続けます。そのため、続きから Xcode のユニットテストを開始することが出来ないです。
そこで Firebase Emulator を実行するスクリプトをバックグランドで実行していました。
emulator_setup.sh
#!/bin/sh
# stop when error
set -e
# for debug
set -x
# check whether firebase has been installed or not.
if ! command -v firebase &> /dev/null
then
# download firebase
curl -sL https://firebase.tools | bash
fi
# setup firebase emulator only for firestore
firebase setup:emulators:firestore
firebase setup:emulators:storage
firebase emulators:start
- 対象のジョブステップ
- name: Setup Firebase Emulator
working-directory: ./firebase
run: ./emulator_setup.sh &
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
- name: Build and Test
run: xcodebuild ...
emulator_setup.sh
の処理を全てバックグランドで実行しつつ Xcode でユニットテストを開始するとユニットテストの開始の方が早い場合があり、うまくテストが回らない結果になったと考えています。
どうするのが良いのか
本題のどうやって解決したかについてです。
まず、firebase emulator:start
は変わらずバックグランドで起動します。そしてfirebase emulator:start
の pid(24535のような数字)
を何らかのファイル A に書き込んでおき、ユニットテストを行う側では pid がファイル A から読み取れるまで待機し続けます。(この時、タイムアウトを設けることもできます。)
そして pid がファイルから読み込めたら Xcode のユニットテストを開始すると言う流れです。
変更点
emulator_setup.sh
(修正)
- firebase_emulator が起動する
- emulator が起動された際の pid をファイルに保存して後に伝える
#!/bin/sh
# stop when error
set -e
# for debug
set -x
# stop when pipefail
set -o pipefail
# check whether firebase has been installed or not.
if ! command -v firebase &> /dev/null
then
# download firebase
curl -sL https://firebase.tools | bash
fi
`echo which firebase` 1>&2
# setup firebase emulator only for firestore
firebase setup:emulators:firestore
firebase setup:emulators:storage
firebase emulators:start &
pid="$!"
echo "$pid" > /tmp/firebase_emulator_pid.pid
wait_firebase_emulator_setup.sh
(新規作成)
- firebase_emulator の起動が開始されるまで待機するスクリプト
#!/bin/sh
if ! command -v gtimeout &> /dev/null; then
brew install coreutils
fi
gtimeout 600 sh -c '
while :; do
if [ ! -s "/tmp/firebase_emulator_pid.pid" ]
then
echo "waiting firebase_emulator_setup completion."
sleep 3
else
break
fi
done \
&& echo "complete firebase_emulator_setup."
'
source.yml(GitHub Actions のファイル)
- firebase_emulator を開始する
-
firebase emulator:start
の pid はファイル/tmp/firebase_emulator_pid.pid
に書き込む
-
- Xcode で UnitTest の実行
-
/tmp/firebase_emulator_pid.pid
からエミュレータの pid を読み込み停止する
jobs:
test:
name: "Build Sources"
runs-on: macos-11
steps:
- uses: actions/checkout@v3
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Install Java17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Firebase Emulator
working-directory: ./firebase
run: ./emulator_setup.sh &
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
- name: Wait firebase_emulator_setup
working-directory: ./firebase
run: ./wait_firebase_emulator_setup.sh
- name: Build and Test
run: xcodebuild ...
- name: Kill firebase_emulator process
run: kill `cat /tmp/firebase_emulator_pid.pid` &>/dev/null
ちなみに公式の Firebase Emulator を起動・停止するスクリプトから今回のアイデアは取ってきたので、そちらも合わせて確認してみてください。
Discussion
firebase emulators:exec
を使う方法もあるのかなと思ったので共有です