XcodeのUnitTestでCloud FirestoreのRead,Writeのテストをしたい場合に、
Firestore Emulatorを使うと実際のプロジェクトを使わずに読み書きのテストができるので非常に便利です。
またこの方法を使うと、テスト用にFirebase Projectを作成する必要もなくなります。
記事の続きで導入方法を紹介します。
(2019/09/29: セキュリティルール周りに関して記事を修正をしました。)
事前条件
- Firebase iOS SDKを導入している
- Unit Testが実行出来る状態である
このあたりは基本事項としてスキップさせていただきます。 🙏
Firestore Emulatorの準備
PCのグローバルな領域でも、プロジェクト用に /firebase
ディレクトリ作ってでも良いので、firebase-tools
をインストールします。
$ npm install -g firebase-tools
その後、Firestore Emulatorをインストールします。
$ firebase setup:emulators:firestore
インストールができたら、次のコマンドでエミュレータを立ち上げます。
$ firebase emulators:start --only firestore
(旧来の、firebase serve --only firestore
では、後述のセキュリティルールの読み込みができないので注意です。)
エミュレータが立ち上がると、デフォルトだとlocalhost:8080
にアクセスすると「Ok」が表示されるかと思います。
これでエミュレータ側の準備は完了です。テストを実行する前にエミュレータを起動するようにすればokです。
エミュレータを実行すると、firebase-debug.log
とfirestore-debug.log
が吐き出されることがあるので、気になる場合は.gitignore
で指定しておくと良いでしょう。
この状態ではセキュリティルールはない状態で、すべての読み書きが許可されている状態になっています。指定したセキュリティルールを読み込ませたい場合は次を参照してください。
セキュリティルールルールを読み込ませる場合
エミュレータを起動する際に、firebase.json
、firestore.rules
を準備すれば、任意のrulesを読み込ませてエミュレータを起動することが出来ます。
例えば、次のようにfirebase
ディレクトリを作り、そこに2つのファイルを次のように作成して配置します。
$ pwd
# path/to/project
mkdir firebase
touch firebase/firebase.json
touch firebase/firestore.rules
- firebase.json
{
"firestore": {
"rules": "firestore.rules"
}
}
- firestore.rules
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
rulesはいわゆるテストモードの例を出したので、ここは所望するルールを書いてみてください。
あとは、firebase
ディレクトリ上でエミュレータを起動すれば、読み込んでくれます。
$ cd firebase
$ firebase emulators:start --only firestore
i Starting emulators: ["firestore"]
i firestore: Serving WebChannel traffic on at http://localhost:8081
i firestore: Emulator logging to firestore-debug.log
✔ firestore: Emulator started at http://localhost:8080
i firestore: For testing set FIRESTORE_EMULATOR_HOST=localhost:8080
✔ All emulators started, it is now safe to connect.
もし、うまく読み込めていない場合は、次のようなログが吐き出されます。
⚠ Could not find config (firebase.json) so using defaults.
i Starting emulators: ["firestore"]
⚠ No Firestore rules file specified in firebase.json, using default rules.
この場合は、ルールを指定しなかった場合と同様に、すべての書き込みが許可されている状態で扱われます。
こちらはmonoさんが教えてくれました。ありがとうございます。
エミュレーターでも `firestore.rules` が読まれて普通にルール効くだろうという認識でしたが、ダメでした?🤔
— mono 🎯 @自宅 (@_mono) September 29, 2019
`firebase serve` 使っているみたいですが、後発の `firebase emulators:start` が推奨というのが僕の認識で、そちら使うと効いたりしませんかね?( ´・‿・`)https://t.co/vvrrPVvNfb pic.twitter.com/QCrAqi1ftJ
UnitTest用FirebaseAppの作成と接続先変更をするための処理を書く
FirebaseTestHelper.swift
を作成し、次のように記述します。
import Foundation
import Firebase
private let dateFormatter: DateFormatter = {
let f = DateFormatter()
f.locale = Locale(identifier: "en-US")
f.dateFormat = "yyyyMMddHHmmss"
return f
}()
enum FirebaseTestHelper {
static func setupFirebaseApp() {
if FirebaseApp.app() == nil {
let options = FirebaseOptions(googleAppID: "1:123:ios:123abc", gcmSenderID: "sender_id")
options.projectID = "test-" + dateFormatter.string(from: Date())
FirebaseApp.configure(options: options)
let settings = Firestore.firestore().settings
settings.host = "localhost:8080"
settings.isSSLEnabled = false
Firestore.firestore().settings = settings
print("FirebaseApp has been configured")
}
}
static func deleteFirebaseApp() {
guard let app = FirebaseApp.app() else {
return
}
app.delete { _ in print("FirebaseApp has been deleted") }
}
}
重要なポイントは、Firestoreのsettings
の書き換えで、
Firestoreの接続先となるhost
をlocalhost:8080
に、isSSLEnabled
をfalse
に書き換えてあげます。
また、FirebaseOptions
を自作することで、実際のGoogleService-Info.plist
は不要になります。
googleAppID
は内部でvalidationが書けられる模様で、ひとまず上記のようなIDを使っておくといいと思います。
projectID
は、テスト毎に被らないようにするために、test-{日付}
のIDを生成するようにしています。
後の項目は不要です。(BundleIDとか)
Testを書く
あとは、テストのsetup/tearDownの部分で先程記述した処理を呼び出した上で、テストを書いていきます。
class SomeFirestoreTests: XCTestCase {
override func setUp() {
super.setUp()
FirebaseTestHelper.setupFirebaseApp()
}
override func tearDown() {
super.tearDown()
FirebaseTestHelper.deleteFirebaseApp()
}
func test() {
let exp = expectation(description: #function)
let userRef = Firestore.firestore().collection("users").document()
userRef.setData(["name": "john"]) { error in
if let error = error {
XCTFail("\(error)")
}
userRef.getDocument { snapsot, error in
if let error = error {
XCTFail("\(error)")
}
XCTAssertEqual(snapsot?.data()?["name"] as? String, "john")
exp.fulfill()
}
}
wait(for: [exp], timeout: 5.0)
}
}
あとはテストを実行するだけです。
事前にエミュレータを起動することをお忘れなく。
使い所
通信部分をスタブできるようになるのでネットの接続、実際のプロジェクトのDBの状態に関係なくFirestoreの処理が絡む部分をテストすることが可能になります。
何かしらのドキュメントを読み込むときは事前にテストデータを作って書き込めば良いので便利です。
※現状の制限事項
もしfirestore.rules
を読み込ませる場合、js-sdkの方にあるfirebase/testing
モジュールとは違って、admin権限でFirebaseAppを初期化する手立てがないので、
ルールを無視してテストデータを突っ込むのはやや大変そうです。
未検証ですが、project-IDをランダムなものではなく何かしら固定のものにして、Xcodeの外側でfirebase/testing
モジュール使って、admin権限でFirebaseAppをセットアップした後にデータを突っ込む、、
といったやり方になるかもしれません。
※セキュリティルール自体を検証したい場合
個人的にはセキュリティルール自体の検証(テスト)をしたい場合は、Xcodeに拘らず、js-sdkの方にあるfirebase/testing
モジュールを使ってテストを書いた方が良いかなと思います。
Xcode上ではあくまでも疎通確認というか、読み書きの部分をスタブさせてあげるくらいの用途がいいのかなと思います。
(もちろんXcode側でもしっかりrulesを適応してテストしていれば異常な書き込みは検知できるので良いですが、シンプルにrulesの検証だったらfirebase/testing
モジュールを使ったテストの方が簡単です。)