RIBsはUberで開発し、オープンソースとして公開したモバイルアーキテクチャフレームワークだ。RIBsフレームワークは、アプリの複雑な状態管理とビジネスロジックをRIBsのかたまり(以下Riblet)に分割し、ツリー構造に連結する。1つのRiblet単位を構成するオブジェクトとそれぞれの役割は以下の通りである。

RIBsとオブジェクト指向プログラミング

上記の図のように、各RIBsオブジェクトは役割がはっきりと分かれている(Single Responsibility)。そして、矢印で示されている各オブジェクトのinputとoutputはプロトコルで抽象化されているので (Dependency Inversion)、 別々に離した後mocking技法を通じて独立してテストするのが良い。 また、親と子Ribletはツリー構造に分離(decoupling)されており、親Ribletのコード修正を最小限に抑えながらも、複雑な子Ribletを新たに作成・修正することができる(Open-Closed Principle)。2か月ほどプロジェクトを進める中で感じたのは、RIBsアーキテクチャを使うとSOLIDのうち比重が大きく、着実に守りにくいSRP、OCP、DIPの3つの原則を反強制的に守ることになる。 もちろん、簡単に違反することもできる。どのアーキテクチャを使うかはとにかく、開発者のコードの作成方法によるもので、アーキテクチャを導入するだけで自分のコードが良くなるわけではない。

何をテストすべきか?

RIBsアーキテクチャを構成するオブジェクトは、単体テストが本当に簡単だ。役割が明確で細かく分かれており、親と子Ribletが疎結合されているだけに、高いカバレッジを達成することができる。Ribletごとに少なくとも4つ以上のテストクラスが必要だが、Xcodeのコード生成テンプレートを使うと、毎回作成するのが面倒な単体テストクラスとボイラプレートコードまで自動的に生成する。ところで、初めてプロジェクトをする時は、この多くのテストクラスに何を入れるのか分からなくて困っていた。 しかし、コードレビューも受け、既存のコードも見ながらルールを発見し、これにより、各クラスの役割と目的に応じてどのようにテストする必要があるかを会得した。

Router Test:子Ribletルーティング

Routerのテストは、ビジネスロジックに合わせて子Ribletを切り取ったり(attachChild)、貼り付けたり(detachChild)する動作を検査する必要がある。Routerのオブジェクトの説明によれば、「ルーターが子ルーターを作る時は必ずhelper builderを使わなければならない」(リンク)というコメントがある。子Ribletを生成する時にxxBuilderクラスを直接使わないで、xxBuildableで抽象化して注入される必要があるということだ。これにより、テスト環境でxxBuildableMockを注入して、ルーターが子Ribletを正しく作成したかを確認できる。Routerはinteractorのリクエストに応じてルーティングをしてくれる役割なので、これ以外のロジックはないことが望ましい。

Interactor Test:各種ビジネスロジック

Interactorは、アプリのビジネスロジックを担当する部分なので、他のクラスより複雑で開発者の自由度が高い。そのため、他のコンポーネントのようにカテゴライズするのは簡単ではないが、よく使用されるものがいくつかある。

Interactorは、子interactorに情報を渡すためにリアクティブプログラミング方式を使用する(注:公式文書)。リアクティブストリームを活用すると、親と子のinteractorが直接的な依存関係(direct coupling)を結ぶ必要がない。ストリームはどのように実装しても開発者の自由だが、通常はRxSwiftを使えば良い。RIBsアーキテクチャもRxを使っており、これを使えばあえてリアクティブライブラリを複数追加する必要はない。テスト環境では、ストリームをmockingしてinteractorが正しく値を渡しているかを確認できる。逆に、親interactorから注入されたストリームにsubscribeしてタスクを処理する場合は、同様にmockストリームを注入した後、テストケースで値を変えながらデータに合わせてうまく処理しているかを検査する。

Interactorは、viewからユーザーの入力を渡される。ユーザーの入力に応じて適切なタスクが実行されたことを確認するために、interactorのview listener関連メソッドをテストケースから直接呼び出して、必要なタスクが実行されたか、mockingされた依存オブジェクトを確認する。

Interactorは、routerとpresenter(またはview)を頻繁に呼び出す。ビューを切り取ったり、貼り付けたり、UIを更新したりする。そのため、routerとpresenterメソッドが意図通り呼び出されることを確認する。この場合には、単に呼び出されたかを確認するよりも、呼び出された回数を正確に検査する方が良い。不要なUIの更新を何度行っているかを確認することもでき、重複でビュールーティングをしてはいないかも確認する。(参考:公式チュートリアルMockオブジェクト)

Builder Test:Concreteクラスの生成と注入

BuilderはRIBsオブジェクトを作成して参照点を連結し、依存性注入に必要なconcreteクラスを生成する役割だ。従って、isやas?を使って正しいクラス型が生成されたかを確認する方法で動作を検査する。

Presenter Test:ビューモデル生成ロジック

データモデルがビューモデルにうまく変換されているのかを確認する。データモデルはUIKitクラスを使わないようにし、ビューを構成する時に必要なUIKit、Core Graphics型などはビューモデルで持っているのが良い。例えば、データモデルはhex文字列でカラー値を持ち、presenter からそれをUIColorに変換する。あるいは、データモデルはDate型の日付を持ち、preserterからDateFormatterを通じて文字列に変換し、ビューモデルに保存する。このような変換ロジックを検査する必要がある。

View Test:ビューのレンダリング

Viewはコードだけでは有意なテストをするのが難しい。スナップショットテストライブラリを使用すると、ビューをレンダリングして画像ファイルとして保存し、テストを実行する時にビューを新しくレンダリングして既存の画像ファイルと比較する方法でビュークラス(UIView、UIViewController)を検査することができる。もし、コードの変更によってビューが変わると、テストケースが失敗するため、ビューが間違っていることを事前に確認することができる。スナップショットテストも単体テストであるため、カバレッジに含まれるのが利点だ。スナップショットテストを追加することで、Ribletのカバレッジを約12〜15%程度増加させることができた。