基本的に外部 API 呼び出ししかないコードをいろいろ分解してだいたい TDD っぽく作ったよという話。今回はフレームワークに頼れる部分がなく、全体のパーツを自覚的に整理しながら作り上げていく必要があったので頭の整理をしておく。自分のやっていることはゼロかイチかではなく、概ねどこかしらにテストコードはあるけど網羅はしていなくて、だいたい今回のような感じで進めているような気がする。
だいたいの流れ
クリーンアーキテクチャっぽい円の外側から攻めていった。何しろ思ったように API が動くのか動かないのか分からないと作りようがないので、まずはそこから。1
- (非TDD)実際の API の動作確認
- (非TDD)API の response を再利用しやすいようにある程度パターン別に収集(テストコードを利用してアプリケーションの一部だけを動かす)
- (TDD)API アクセスのコードの一部を stub out しながら API Client 部分を実装
- できる範囲で API の動作を mock サーバや emulator で閉じ込めて「分離」
- (TDD)API に依存しつつアプリケーション全体の文脈を強く表すボキャブラリーとインターフェイスを用いた Facade 的なものを用意2
ここまでは stub / mock / emulator でどうにかなった。
気をつけていたのは Gateway 的な部分で、例えばストレージ系の API を呼ぶ必要のある API Client の部分は Repository パターンのような汎用的なインターフェイスを用意しつつ、もう一つ内側の Gateway の部分では Repository をあまり意識させず、単にやりたいことをメソッドとして持つようにしておいた3。
どうにかならない部分は処理の流れ的にほぼ後行程にしかないので文字通り後回しにした。これはそうなるようにアーキテクチャを選んでいたからできたことでもある。
そしてようやくアプリケーション全体の流れ。
- (非TDD) Application Logic (API の初期化と処理全体の流れ)の実装。最低限の初期化周り以外はほとんど自動テストしていない。
今回これで回せた理由は以下の二つだと思っている。
一つめは、そもそも今回はアプリケーションをサーバレスのピタゴラスイッチで実現することにしたこと。一つ一つのアプリケーションの役割は十分に小さいのでミスが入りにくい。
もう一つは、流れに関するロジックを図で言うところの Gateway か Domain に押し付けるようにしたから。
例えばアプリケーションロジックのように見えるものでも API 呼び出しの比率が高いものは Gateway の中に閉じ込めることで API 側で回せている TDD の中で完結させることができるならできるだけそうしたのだ。全体の流れとしては何らかのメソッドを呼んでいるだけにする。
逆に完全にプレーンなオブジェクトだけで表現可能なものは、
- (TDD)プレーンなオブジェクトに対して API に依存するオブジェクトからメソッドを通じて値オブジェクトを渡す形で独立させてロジックを実装
していった。
この部分が最初から見えていてプレーンなオブジェクトに抽出できていたらドメイン駆動っぽいのだけど、残念ながら自分の場合は API call 部分からどうやって testable にするか順番に引き剥がしていって、全体の流れのロジックを書く段階になって完全に逆サイドにロジックを押し付けるという戦術に結果的になった。
まとめ
アプリケーション全体を以下のように分解して考えることで、外部 API call だらけのアプリケーションでも開発の中盤はだいたい TDD できることが分かった。
- そもそものアプリケーションの流れを小さく分解
- API call 部だけの TDD
- API call とアプリケーション全体の流れの中間に入る部分の TDD(Gateway)
- 一切の API call と距離を置くプレーンなオブジェクトでロジックを記述する部分の TDD
今回は手元で再現できる emulator や mock サーバ、抽象化するライブラリのある API を自分で選ぶことができたが、そうでない場合はテスト環境を TDD のための装置として
- まずは deploy と環境構築の自動化
からやる必要があるだろう。それすらできない場合は…どうするのがいいんだろう。