Mockito-Kotlinを使ってテストコードを書いてみる

[RetailAI Adventurers Advent Calendar 2023](https://qiita.com/advent-calendar/2023/rai-adventurers)の7日目の記事です。

Mockito-Kotlinの基本的な使い方をまとめてみます。

Javaなどでテストダブルを作成できるMockitoというライブラリがあります。
以前、KotlinとSpring Bootを使ったサンプルアプリのテストコードを書くためにMockitoを使ってみたことがあります。せっかくKotlin用のライブラリがあるので、当時のメモをもとにMockito-Kotlinで書き直してみました。

## テスト対象のコード

```kotlin
@Service
class UserService {
    @Autowired
    lateinit var repository: UserRepository

    fun getUserById(id: Int): User? {
        return repository.findById(id).orElse(null)
    }
}
```

このクラスは以下の`UserRepopsitory`クラスに依存しています。
JPAリポジトリを利用してデータベースにアクセスするコードです。
（自分でテスト用のクラスを作ろうとすると、オーバーライドしないといけないメソッドが多くて結構大変）

```kotlin
@Repository
interface UserRepository: JpaRepository<User, Int> {}
```

`UserService`だけをテストしたいので、`UserRepository`をモック化したい。

## Mockit-Kotlinを使ってみる
build.gradle に以下の依存関係を追加。

```groovy
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.2.0'
```

### テストクラスを作成
```kotlin
class UserServiceTestByKotlin {
    @Mock
    lateinit var mockUserRepository: UserRepository

    @InjectMocks
    lateinit var userService: UserService

    @BeforeEach
    fun init() {
        MockitoAnnotations.openMocks(this)
    }
}
```

`@Mock`でmock化するクラスを指定。
`@InjectMocks`でmock化するクラスに依存しているクラスを指定する。

### モックを使ったテスト

```kotlin
@Test
fun shouldInvokeFindById() {
    userService.getUserById(10)
    userService.getUserById(20)
    userService.getUserById(20)

    verify(mockUserRepository).findById(10)    ///(1)
    verify(mockUserRepository, atLeast(1)).findById(10)

    verify(mockUserRepository, times(3)).findById(anyInt())
}
```

`verify()`でメソッドが呼び出された回数のテストを行う。
xx回、最大(最小)xx回などを第2引数で指定する。
`///(1)` は第2引数に`times(1)`を渡したのと同じ動作らしい。

### スタブを使ったテスト
```kotlin
private val testUser = User(1, "taro", 33)

@Test
fun shouldReturnUserDetailFromRepository() {
    whenever(mockUserRepository.findById(1))
        .thenReturn(Optional.of(testUser))
        
    val user = userService.getUserById(1)
    assertThat(user!!.id).isEqualTo(1)
    assertThat(user.userName).isEqualTo("taro")
    assertThat(user.age).isEqualTo(33)
}
```

`whenever()`と`thenReturn()`を使う。
戻り値を指定しない場合、`null`（プリミティブ型なら初期値）になっている。

```kotlin
@Test
fun stubDefaultValue() {
    val user = userService.getUserById(2)
    assertThat(user).isNull()
}
```

以下のようにも書ける。

```kotlin
mockUserRepository.stub {
    on { findById(1) } doReturn Optional.of(testUser)
 }
```

## JavaでMockitoを使ったときの違いなど

### `when`と`whenever`

Stubで値を返す値を指定するとき、Mockitoでは`when`を使う。
しかしKotlinでは`when`が予約語になっているので、`whenever`を使えるようになっている。
ちなみに、[ソースコード](https://github.com/mockito/mockito-kotlin/blob/1ce3617f9105142dabc2f20d64d36f70361b55da/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/OngoingStubbing.kt#L43)を見ると`when`の糖衣構文らしい。
```kotlin
@Suppress("NOTHING_TO_INLINE")
inline fun <T> whenever(methodCall: T): OngoingStubbing<T> {
    return Mockito.`when`(methodCall)!!
}
```

### `any()`

```kotlin
whenever(mock.example(any(User::class.java))).thenReturn(11)
```
とか
```kotlin
verify(mock).example(any(User::class.java))
```
などが、`NullPointerException`になる。

「`any()`などが使えるようになる！」て堂々と書いていたのに......？と思ったが、上のコードで使っていた`any()`はMockitoで定義されているものだった。

Spring Bootのプロジェクトを作ったときに追加していた依存関係
```groovy
testImplementation 'org.springframework.boot:spring-boot-starter-test'
```
この中で読み込まれるパッケージにMockitoが含まれており、MockitoとMockito-Kotlinの`any()`が衝突していたらしい。
（だから、引数を渡さないとIDEAがエラーを表示していた）


```kotlin
whenever(mockUserRepository.example(org.mockito.kotlin.any())).thenReturn(11)
mockUserRepository.example(testUser)
verify(mockUserRepository).example(org.mockito.kotlin.any())
```
こんな感じで、パッケージ名まで明示してやると動くようになった。


## 参考
- [Mockito-Kotlinのリポジトリ](https://github.com/mockito/mockito-kotlin)
    wikiを見ると基本的な利用方法が書かれている。

- [Spring Bootでテストコードを書いてみる
](https://qiita.com/a-pompom/items/3f834119c756e5286730)

- [Spring Bootでmockitoを使ってテストする方法](https://confrage.jp/spring-boot%E3%81%A7mockito%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%86%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B/)