Mock を極力使わないようにしていた私が Mockito を使わないとムズムズするカラダになった理由

2020.05.28 追記: やっぱり mock 使わない方が良さそうに思えてきましたので、この記事から考えが変わってます

はじめに

以前、私の著書では

時間が限られた中、本番に使用しない Mock オブジェクトを作成するよりも、依存する実クラスを使用してテストケースを増やした方が時間もかからず、効率的にテストが行えると思いませんか?

とか書いていたのですが、最近はそうでもなくなっているので書き散らかしてみようと思います。 (10年以上前に書いた奴ですからね...しょうがない)

前提条件

  • SpringBoot 使った Kotlin アプリのサーバサイド(Java でも同じことができます)
    • UseCase とか Repository とか layer に分けて DI します

どんなテストでムズるか?

例えば Controller がこんな流れだったとします。

http://www.plantuml.com/plantuml/png/SoWkIImgAStDuKfCBialKd04aLnWKa7NJi6vA3Mn93KaiJZRiI3JEJ-lf2W_9oUrY0kxE9gMqE9KvyJYL0KhXMIu65TUVacgGb5cUaQ9bG9CmPFTIr_Ex7tSkEvf-xBd4zeUDwv_shdfMSji1PpssEXYjQSejRWWFwyu5U81X2fC3pYavgK0VGq0

CreateTaskApiController の責務として、CreateTaskUseCase#createTask の呼び出し & 結果によるレスポンスのハンドリングがあると思います。 (SpringBoot だと Bean Validation を使った validation も責務になりますがここでは割愛します)

CreateTaskApiController では受け取ったリクエストパラメータを元に CreateTaskUseCase#createTask のパラメータを生成します。 パラメータ生成処理自体はメソッド化なり他のクラスに処理を移譲することで Unit テストが容易に書けるでしょう。

ただ、CreateTaskController が CreateTaskUseCase#createTask に対して「想定通りのパラメータを設定したか?」の観点でテストを行う時はどうでしょう? UseCase 以降でパラメータをそのまま永続化するのであれば永続化したデータを参照することで確認はできるかもしれません。 ですが、Controller の Unit テストなのに永続化したデータまで意識するのは責務を超えている感があります。

Mock を使う

そこで Controller の Unit テストに Mockito を使って UseCase を Mock 化します。

コードはこちらです

// setup
val taskUseCaseResult = TaskUseCaseResultFixtures.create()
whenever(createTaskUseCase.createTask(any())).thenReturn(taskUseCaseResult)

↑は、引数関係なく呼び出されたら TaskUseCaseResult インスタンスを返す Mock として定義しています。

で、Controller 呼び出しを行います。 (Controller なので MockMvc 経由で呼び出していますが、UseCase の Unit テストであればメソッド呼び出しで構いません)

事前に定義した Mock が想定した結果を返すのは当たり前なので Mock の呼び出しを verify します。 観点としては、Mock の引数が想定通りか? と言う点です。

// mock のパラメータに対する verify
argumentCaptor<CreateTaskUseCaseParameter>().apply {
    verify(createTaskUseCase).createTask(capture())
    val capturedParameter = firstValue
    val expectedParameter = parameter.toParameter()
    assertThat(capturedParameter).isEqualTo(expectedParameter)
}

↑ capture キャプチャすると、Mock 呼び出し時点のインスタンスが取れるので、そのインスタンスが想定通りか verify を行います*1

この観点が抜けていると、ただテストを通しているだけになるので価値が半減します。any() でお茶を濁さないようにしましょう*2。 mock 使ったら引数の verify も忘れずに。

また、インスタンスのプロパティ1つずつ verify するとプロパティ増えた時にテストの追従を忘れる可能性があるので、まるっと比較する方が後からテスト壊したことに気付きやすくなるのでオススメです*3

テストクラスのメンテナンスコストを意識すると...

Controller の Unit テストをするなら、Controller の責務を超えた verify は避けた方が良いと思います。

API 呼び出しから一連の流れは Integration テストで行うべきで、この時は永続化した内容、発行したイベント等を verify する必要があるでしょう。 Controller の Unit テストなのに実クラスを DI していると RDBMS の構造が変わってしまった時にテストクラスにも影響を及ぼします。

これはテストクラスの肥大化にもつながり、テストクラスのメンテナンスコストが余計にかかります。

Unit テスト / Integration テストそれぞれの観点のテストクラスを作る必要があると思っているので、

  • Unit テスト: Mock をフルに使い、テスト対象クラスのメソッドだけ意識する
    • Repository の Unit テストに関しては、実際の RDBMS (やコンテナ上の DynamoDB Local とか S3Mock)にアクセスする
  • Integration テスト: 必要な箇所だけ Mock にして API 呼び出しから、レスポンスや永続化したデータの verify を行う

とした方が、ケース毎にどこまでやるのか考えずに済むと思います。

まとめ

「呼び出し先のメソッドのパラメータを正しく渡せているか」を気に始めたら、Mock 好きじゃなかった私も Mock 使わないとムズムズするようになった、と言うお話でした。

これは Controller だけでなく、UseCase や Application Service、Domain Service でも同じことが言えます。 他言語の Mock フレームワークで capture 相当の機能ってあるんでしょうか?あれば他言語でもストレスなく開発できそうだなぁ。

とはいえ、Mockito の version up に追従しなきゃいけなくなった時は「できるだけ使わない」に戻りそうですけどね...

*1:このケースは素直に verify すれば良いのですが、capture 使うサンプルとして書きたかった

*2:自戒も込めて

*3:equals メソッドの実装に影響を受けるのでケースバイケースかもしれません