kotlin-reflectでプライベート関数のユニットテストを制する
これはなに?
Kotlinプライベートメソッドのユニットテストの書き方がわかるよ
Privateメソッドのテスト
「Privateメソッドは公開I/Fを通じて間接的にテストすべき」が一般的な気がしていますが、それでもプライベートメソッドのユニットテストを書きたいときがあります。
軽く調べてみたところ、Javaのリフレクションを使ったサンプルはありましたが、KotlinのReflectionを使ったサンプルは見当たらなかったのでそちらを利用してやってみます。
引数なしプライベートメソッドの場合
まずは引数を取らないプライベートメソッドのユニットテストからいきます。
1. サンプル用意
サンプルとして、以下のプライベートメソッドを持つクラスがあり、isValid
をテストするとします。
package org.example.app
class MyService {
fun execute(): String {
return if (isValid()) {
"OK"
} else {
"NG"
}
}
private fun isValid(): Boolean {
return true
}
}
2. Gradleに依存関係追加
プライベートメソッドをアクセス可能にするために以下のkotlin-reflectを依存関係に追加します。
dependencies {
testImplementation(kotlin("reflect"))
}
3. テスト書く
テストは以下のように書けます。
主なポイントは以下です。
-
MyService::class.declaredMemberFunctions
でMyService
クラス内のメソッドにアクセスし、テストしたいプライベートメソッドをname
で探す。 -
isAccessible=true
とすることで、テスト対象のプライベートメソッドをアクセス可能にする。 -
isValid.call(myService)
のようにプライベートメソッドを持つクラスのインスタンスを渡して実行。
package org.example.app
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.reflect.KFunction
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.jvm.isAccessible
class MyServiceTest {
private lateinit var myService: MyService
private lateinit var isValid: KFunction<*>
@BeforeEach
fun setUp() {
myService = MyService()
isValid = MyService::class.declaredMemberFunctions.first { it.name == "isValid" }.apply { isAccessible = true }
}
@Test
fun `should be true`() {
assert(isValid.call(myService) == true)
}
}
引数ありプライベートメソッドの場合
次に引数を取るプライベートメソッドのユニットテストです。
1. サンプル用意
先ほどのコードに引数としてname
を追加したisValid
をテストするとします。
package org.example.app
class MyParameterService {
fun execute(name: String): String {
return if (isValid(name)) {
"OK"
} else {
"NG"
}
}
private fun isValid(name: String): Boolean {
return name == "Apple"
}
}
2. Gradleに依存関係追加
ここは先ほどと同じなので省略。
3. テスト書く
テストは以下のように書けます。サンプルコードのように、引数をcall
の第2引数に渡すことでパラメタありのプライベートメソッドのユニットテストが可能です。
package org.example.app
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.reflect.KFunction
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.jvm.isAccessible
class MyParameterServiceTest {
private lateinit var myParameterService: MyParameterService
private lateinit var isValid: KFunction<*>
@BeforeEach
fun setUp() {
myParameterService = MyParameterService()
isValid = MyParameterService::class.declaredMemberFunctions.first { it.name == "isValid" }.apply { isAccessible = true }
}
@Test
fun `should be true`() {
// NOTE: 第2引数にパラメータを渡す
assert(isValid.call(myParameterService, "Apple") == true)
}
@Test
fun `should be false`() {
// NOTE: 第2引数にパラメータを渡す
assert(isValid.call(myParameterService, "Banana") == false)
}
}
おまけ
拡張関数の場合
拡張関数も同様に可能なのかついでに調査しました。
結論可能です。
1. サンプル用意
class MyExtensionService {
fun execute(name: String): String {
return if (name.isValid()) {
"OK"
} else {
"NG"
}
}
private fun String.isValid(): Boolean {
return this == "Apple"
}
}
2. Gradleに依存関係追加
ここは先ほどと同じなので省略。
3. テスト書く
テストは以下のように書けます。
MyExtensionService::class.declaredMemberExtensionFunctions
で、MyExtensionService
内に定義されているプライベートな拡張関数を取得する点以外ほとんど同様にテストが可能です。(プライベートメソッドはdeclaredMemberFunctions
で取得していた)
package org.example.app
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.reflect.KFunction
import kotlin.reflect.full.declaredMemberExtensionFunctions
import kotlin.reflect.jvm.isAccessible
class MyExtensionServiceTest {
private lateinit var myExtensionService: MyExtensionService
private lateinit var isValid: KFunction<*>
@BeforeEach
fun setUp() {
myExtensionService = MyExtensionService()
// NOTE: declaredMemberExtensionFunctions で拡張関数を取得
isValid = MyExtensionService::class.declaredMemberExtensionFunctions.first { it.name == "isValid" }.apply { isAccessible = true }
}
@Test
fun `should be true`() {
// NOTE: 第2引数にパラメータを渡す
assert(isValid.call(myExtensionService, "Apple") == true)
}
@Test
fun `should be false`() {
// NOTE: 第2引数にパラメータを渡す
assert(isValid.call(myExtensionService, "Banana") == false)
}
}
Discussion