ハロー、@seto_hiです。
最近は本業でも副業でも3年以上前に作られた既存プロダクトの負債解消を行っています。
負債になってしまった実装に対して、mockkのslotを使ったテストによって動作の同一性を保証しながらリファクタリングしていく方法について紹介します。
どんな場面で使えるか
ほぼブラックボックスになっている負債があるとします。
負債クラスのメソッドを叩くと値が返ってくるようなものだと結果をassertするだけでテストができますが、そうもいかない場合があります。
例えば通信周りが負債になっており、サーバーにどのようなリクエストを送信しているかわからない場合などです。
そのような場合であっても、その負債が最終的に呼ぶクラスがmockやspyができるようになっており、呼ばれるメソッドに引数がある場合に使えます。
例えばOkHttpClientであればnewCallというメソッドの引数にRequestがあります。
OkHttpClientをmockすることができれば、newCallの際に呼ばれたRequestの値をslotによって取得できます。
サンプルリポジトリ
こちらのリポジトリに実装しています。
特に、このPRが記事の内容を実装したものになります。
どんな状態か
ApiにpostするBadImplementationClass
が負債になっており、手入れできない状態です。
(サンプルコードの BadImplementationClass
はあまり負債になっていませんが、心眼で置き換えて下さい。)
それを新しいコードに置き換えるお仕事を任された状態です。
実践
前提を確認したところで、手を動かします。
手順0: Respsitoryを作成する
一番最初にRepositoryクラスを作成して、負債であるAPIを叩くクラスをラップします。
Recommended app architecture に近づくというメリットもありますが、リファクタリング後も変わらないクラスを作成することでテストによる動作の同一性が保証できます。
手順1: テストを導入する
前述の通り、ブラックボックス(負債)の最終的なアウトプットをするクラスがmockできるようなパターンの場合に使うことができます。
今回はOkHttpClientが最終的なアウトプットをするのでこちらをmockし、OkHttpClientProviderがmockしたクラスを返すようにします。
val okHttpClient = mockk<OkHttpClient>() {
every { newCall(any()) } returns mockk(relaxed = true)
}
every {
OkHttpClientProvider.client
} returns okHttpClient
OkHttpClientはnewCallで実際の通信を行うので、このメソッドの引数を保証しておけばブラックボックス(負債)を置き換えられるのですが、ここがslotの出番です。
slotメソッド呼んでインスタンス生成をしverifyの中でcaptureをしてあげると、verifyが呼ばれた際の値を取得することができます。
val slot = slot<Request>()
verify(exactly = 1) {
okHttpClient.newCall(capture(slot))
}
この結果をassertしてあげることによって、どのような値でメソッドが叩かれたかを保証することができます。
val capturedRequest = slot.captured
assertThat(capturedRequest.header("header-name")).isEqualTo("header-value")
// 以下もassert文が続く
手順2: 負債を書き換える
テストが書けたのであとはコードを書き換えるだけです。
Repositoryのテストを書いているので、Repository層以下を書き換えてもテストが通るのならば動作は保証されます。
今回はRetrofitを導入してみました。
Requestに書いているheaderとbodyが一致していることはテストによって保証できます。
今回はうまく行くようなケースにしていますが、例えばheaderをInterporatorで追加したい場合などはもう少し複雑になります。
(InterporatorはnewCall以後に呼ばれるため)
手順3: 完
これにてリファクタリング完了です。
お疲れ様でした!