Razl-Dazl

eyecatch

KClassを使ってメソッドを呼び出してみる

Posted at — 2022-06-27

最近JUnit5を使うことが多いのですがメソッドの呼び出しがややこしく毎回つまづいてしまうので、自分の中で少し整理してみました

1. publicなクラスのメソッドを取得する

ここでは以下のコードをテストします

class ClassA {
    fun publicFunction(): String {
        return "Public fun of ClassA"
    }

    private fun privateFunction(): String {
        return "Private fun of ClassA"
    }

    companion object {
        fun companionPublicFunction(): String {
            return "Public fun of companion object of ClassA"
        }

        private fun companionPrivateFunction(): String {
            return "Private fun of companion object of ClassA"
        }
    }
}

1-1. puclicメソッドの取得

これが基本形になります

  1. テスト対象クラスをKClassのオブジェクトで取得
  2. インスタンスの生成
  3. テスト対象メソッドの取得(一覧をリスト形式で取得してから、メソッド名で絞込み)
  4. テスト対象メソッドの実行
@Test
fun publicFunction() {
    // ①
    val clazz: KClass<ClassA> = ClassA::class
    // ②
    val instance = clazz.createInstance()

    // ③
    val function = clazz.functions
        .first { it.name == "publicFunction" }

    // ④
    val result = function.call(instance)
    val expect = "Public fun of ClassA"
    assertEquals(expect, result)
}

1-2. privateメソッドの取得

  • functionsの代わりにdeclaredFunctionsを使用すると、privateなメソッドも取得可能になります
  • また、対象メソッドにアクセスするため、isAccessibleプロパティを書き換えています
@Test
fun privateFunction() {
    val clazz: KClass<ClassA> = ClassA::class
    val instance = clazz.createInstance()

    val function = clazz.declaredFunctions
        .first { it.name == "privateFunction" }
        .apply { isAccessible = true } // privateメソッドへのアクセスに必須

    val result = function.call(instance)
    val expect = "Private fun of ClassA"
    assertEquals(expect, result)
}

1-3. companion object内に定義されたメソッドの取得

  • companion object内に定義されたメソッドを取得する際は、インスタンスの生成方法とメソッドの取得方法が異なります

@Test
fun companionPublicFunctionTest() {
    val clazz: KClass<ClassA> = ClassA::class
    val instance = clazz.companionObjectInstance

    val function = clazz.companionObject!!.functions
        .first { it.name == "companionPublicFunction" }

    val result = function.call(instance)
    val expect = "Public fun of companion object of ClassA"
    assertEquals(expect, result)
}

1-4. companion object内に定義されたprivateメソッドの取得

  • 注意事項は 2. と同一です
@Test
fun companionPrivateFunctionTest() {
    val clazz: KClass<ClassA> = ClassA::class
    val instance = clazz.companionObjectInstance

    val function = clazz.companionObject!!.declaredFunctions
        .first { it.name == "companionPrivateFunction" }
        .apply { isAccessible = true }

    val result = function.call(instance)
    val expect = "Private fun of companion object of ClassA"
    assertEquals(expect, result)
}

2. privateなクラスのメソッドを取得する

今度は以下のコードをテストします

class ClassB {
    private class Sub {
        fun publicFunction(): String {
            return "Public fun of ClassB"
        }

        private fun privateFunction(): String {
            return "Private fun of ClassB"
        }
    }
}
  • companion objectは取得できなかった為に省いています
  • どうしても取得したい場合は、KClassではなくJavaのClassで取得する手もあります(ここでは割愛)
  • しかしそのような場合は、Classで取得するよりも、コード自体の修正をした方が良いかと思います・・・

2-1. publicメソッドの取得

  • テスト対象クラスがprivateなので親クラスから間接的に取得する必要がありますが、手順についてはメソッドの取得方法と似ています
  • コンストラクタに直接アクセスできない為、インスタンスの生成方法はpublicなクラスの場合とは異なります
    1. コンストラクタ取得
    2. isAccessibleプロパティを操作してアクセス可能にする
    3. コンストラクタのcall()を呼び出してインスタンスを生成する
  • テスト対象メソッドがpublicな場合でも、メソッドのisAccessibleプロパティは変更する必要があります
@Test
fun publicFunction() {
    val clazz = ClassB::class.nestedClasses
        .first { it.simpleName == "Sub" }

    val constructor = clazz.constructors
        .first()
        .apply { isAccessible = true }
    val instance = constructor.call()

    val function = clazz.functions
        .first { it.name == "publicFunction" }
        .apply { isAccessible = true }

    val result = function.call(instance)
    val expect = "Public fun of ClassB"
    assertEquals(expect, result)
}

2-2. privateメソッドの取得

  • メソッドの取得でdeclaredFunctionsを使用している箇所以外は、publicの場合と同一です
  • 本当はdeclaredFunctionsにせずfunctionsのままでも問題なく動いたんですよね・・・うーん・・・?
@Test
fun privateFunction() {
    val clazz = ClassB::class.nestedClasses
        .first { it.simpleName == "Sub" }

    val constructor = clazz.constructors
        .first()
        .apply { isAccessible = true }
    val instance = constructor.call()

    val function = clazz.declaredFunctions
        .first { it.name == "privateFunction" }
        .apply { isAccessible = true }

    val result = function.call(instance)
    val expect = "Private fun of ClassB"
    assertEquals(expect, result)
}

3. メソッド呼び出しの補足

メソッドに引数が必要な場合

    fun foo (arg1: String, arg2: Int, arg3: Boolean): String {  
        return bar()
    }

テストの際は、call()の2番目以降の引数に値を入れていけば問題ありません

    val result = function.call(instance, arg1, arg2, arg3)

4. インスタンス生成の補足

インスタンス生成時に引数が必要な場合

private class Sub(val arg1: String, val arg2: String) {   
}

メソッドの時と同様、call()の引数に値を入れていけば問題ありません

val instance = constructor.call("hello", "kotlin")

本当はプロパティへのアクセスとかについても書きたかったのですが、ちょっと長くなってしまったので今回はやめておきます

ご参考までに・・・

Author@zakuro

Mastodon: 396@vivaldi.net