Firebase Realtime DatabaseをiOSで扱う際のMockについて考えてみた
こんにちは。松館 (@d_date )です。 この記事はアラスカのタイムゾーンで書かれていますので、現在5日朝の8時です(うそです遅くなってすいませんでした)。
というわけでこの記事は Firebase Advent Calendar 2017 の 6日目の記事です。また、iOS Test Night #6–1周年 - の登壇まとめ、先日の serverlessConf Tokyo 2017 の登壇内容の補足も兼ねています😉
さて、Firebase Realtime Database は、Firebase が出しているDatabaseのひとつです(10月にCloud Firestore が出ましたが、今日は触れません)。
実際のプロダクトに投入しようとしたときに気になるのは、正しくデータがマッピングされ、正しくアプリで扱えるかです。
NoSQLは非正規化が原則
Realtime DatabaseはNoSQLなデータベースで、その構造はJSONツリーです。NoSQLは非正規化していくのが原則で、できるだけ階層を浅くします。
ある構造には通常他の構造を参照するためのキーが含まれており、必要アクセスが必要になったら随時取りに行きます (ただし、キャッシュを有効にしている場合、すでに取得済みのパスに関しては通信は発生しません)。
これは日頃Viewで必要とするデータ構造と通常異なるため、取得したデータをアプリで使用するデータに変換することが必要となります。
これをDomain Layer と Data Layer と見なせば、適応するアーキテクチャの要件は自ずから出てくるよね、という話を serverlessConf Tokyo 2018でして来たので、興味がある方はこちらの記事も参照ください。
Realtime Databaseをテストする?
その場で深く話が出来なかったのですが、Unit Test という観点では、
- Realtime Database から取得したオブジェクトが正しく指定の型に変換されること
- クライアントサイドでJOINが正しく行われること
を担保する必要があります。
ですが、現実的には、
- FirebaseAppの初期化が必要
- 実行環境によってはネットワークが同一ではない
- アクセスするまで構造が不定 (アプリ外にDBがあるということは、アプリからみればシュレディンガーの猫です)
- データが動的 (毎回のテストでデータが同じ、とは限らない)
など、環境が変化することがあり得ます。
※じゃあ開発用の環境を用意すればいいのでは?と思われるかもしれませんが、複数人が同じ環境でテストを行った場合、環境が同一のまま変化しないことは担保できません。
動的な環境要因の排除
そこで
- FirebaseAppの初期化がいらず
- オフラインでも使えて
- 構造はあらかじめ分かっている状態で
- データも固定
という観点で、Firebase へのアクセスをMockにしようと思いつきます。
もうひとつここで考えておきたいのが、テストをするために過剰なコードは書きたくないということです。
今回の場合だと、アプリからは通常通りFirebaseにアクセスさせながら、Unit Test ではStubとなるjsonをあらかじめローカルに置いておいて、それを取得するようにしたいと思います。
ターゲットによって読み込むファイルを分岐する
たいていの場合、アプリとUnit Test のターゲットは別になっているので、読み込むファイルをターゲットに合わせて分けてあげればよさそうです。
このとき、アプリが適切に設計されていれば、インターフェースは同一だが、ターゲットによって振る舞いが変わる環境が実現できるはずです。
具体例を見てみます。Realtime Database のあるパスを取得し、指定した型に変換するメソッドです。Realtime Database の observeEvent
, observeSingleEvent
がRxSwiftの Observable
, Single
と相性が良いので、RxSwiftでラップしてあげます。
一方Unit Testのときは、Bundleからjsonファイルを指定して取得し、Decodeしてあげます。
Swift4からCodableが入ったので、Decodeもとても簡単ですね😎
型を偽る
また、この際、Firebase Realtime Database に関する型はいくつかtypealiasで偽る必要があります。設計にもよりますが、以下のように置き換えてみます。
まず、FirebaseAppの初期化はとらないですが、Databaseの初期化にこの型を使っています。FirebaseAppのオブジェクト自体は必要ないですが、コンパイルを通すために Any にします。
また、timestampを取得する ServerValueという型も、Anyを返すtimestamp()というメソッドを持ったFakeに置き換えます。そしてもともとアプリ側ではFRDRequestというDatabase にアクセスするDAOを用意していたので、これを先ほどStubからjsonを読み込むようにした機能を持たせたFRDRequestMockに置き換えます。なんだかコンパイラを騙してるようですね🤫
このように適切に設計することで、Interfaceは同一に保ちながら、Stubなデータを取得することが可能です。
この設計による展望
このように設計しておけば、例えばFirestoreへの移行を行った際に、Viewへの影響を減らすことができたり、常に固定なデータを使ったUI Testの構築もしやすいのではないか、とテスト初心者ながらに思っています。
まとめ
さて、Firebase Realtime Database におけるモックについて考えてみましたが、たどり着いたところはアプリの設計の話になりました。不思議ですね🤔
すでにFirebaseに限らず様々なBaaSが登場していますが、どのサービスをプロダクトに採択するに当たっても、
- 外界との境界は抽象化する
- ターゲットによってインターフェースの振る舞いを変えると楽
という考え方は有効に作用しそうです。もちろんアプリの規模感、要件によって設計は変わって行きますし、Layered されたアーキテクチャが銀の弾丸である、と言いたいわけではありません。要件に合わせて設計した方が長期的にみておトクなこともあるよ、というのがここ最近一貫してお話していることでした。
というわけで、この辺りでおしまいとします。ありがとうございました!