Firebase Realtime DatabaseをiOSで扱う際のMockについて考えてみた

Daiki Matsudate
7 min readDec 5, 2017

--

こんにちは。松館 (@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 されたアーキテクチャが銀の弾丸である、と言いたいわけではありません。要件に合わせて設計した方が長期的にみておトクなこともあるよ、というのがここ最近一貫してお話していることでした。

というわけで、この辺りでおしまいとします。ありがとうございました!

--

--