ls -al

仮想通貨やプログラミングに関する事などをつらつらと書き綴ります

KotlinのObjectのインスタンスを書き換える

あまり褒められたものではないのですが、必要に迫られたので調査し、やってみました。


まえがき

 この記事ではKotlinのObjectのインスタンスを書き換える方法を紹介しますが、本来そのようなことはしない方が良いです。書き換える必要が発生した時点で設計を見直した方が良いです。この事を念頭に置いて読んでください。


方法

以下のような関数を使います。

fun changeObjectInstance(target: Any, mock: Any) {
    val kClass = Class.forName(target::class.qualifiedName).kotlin
    val field = kClass.java.fields.first()
    val modifiersField = Field::class.java!!.getDeclaredField("modifiers")
    modifiersField.isAccessible = true
    modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv())
    field.isAccessible = true
    field.set(null, mock)
}

 Objectのインスタンスを書き換えられるようにするために、リフレクションを活用してINSTANCEのfinalを外したりして無理やり書き換えてしまいます。


使用例

 これは関数の引数名にmockとあるように、テスト時にObjectのインスタンスをmockに差し替えるために使うことができます。例えば以下のようなFooオブジェクトとそれを利用するHogeクラスがあります。

object Foo {
    fun bar() = 1
}

class Hoge {
    fun huga() = Foo.bar()
}

この時、

Hoge::huga()を実行した時、Foo.bar()が1度だけ呼ばれている事

という事を確認できるテストをしたいと考えたとします。プロパティとして定義されている変数であれば、Mockitoでfiled injectionしてやれば良いのですが、Objectを使ってしまっているとそうもいきません。そこで上記のchangeObjectInstanceを使います。Objectのmockを作成して、以下のようにします。

changeObjectInstance(Foo, mockFoo)

こうする事で、Objectのインスタンスがmockに書き換えられました。あとはMockitoであれば、

val hoge = Hoge()
hoge.huga()
Mockito.verify(mockFoo,times(1)).bar()

としてやる事で、所望のテストを書くことができます。テスト実行後は他のテストに影響しないように、書き換えたインスタンスを元に戻しておきましょう。


背景

 携わっているプロジェクトにて、なんとかテストを導入していきたいという思いがあったのですが、シングルトンオブジェクトをいたるところで多用しており、それらを直接使わないように書き換えるにはあまりに多くの工数が発生することが予見されたため、このような方法を用いることとなりました。


おわりに

リフレクションを使いすぎるとコードの可読性が悪化するなど、デメリットが多数存在します。くれぐれも、用法用量を守って使ってください。冒頭にも書いたように、可能であれば設計を見直すなど、使わない方向での解決策を模索する事をお勧めします。


参考

https://stackoverflow.com/questions/3301635/change-private-static-final-field-using-java-reflection