🔥

Firebase EmulatorをGitHub Actionsで動かしてUnitTestを行う手法について

2022/06/20に公開
1

始めに

この記事では 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:startpid(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