はじめに
筆者はkotlinビギナーです。現在、習うより慣れろの方針で、手を動かしてkotlinをキャッチアップ中です。
今回は 先日の投稿 にて作成した DIMock というクラスをkotlin化し、
そこで得た学びや気づき(個人的に印象に残ったもの)をまとめました。
変換方法
変換については、最初にAndroidStudioの自動変換機能を使い、ビルドエラーや実行時エラーを手動で修正しました。
印象に残った、学びや気づき
javaのソースをコピーし、kotlinのソースにペーストすると、AndroidStudioがkotlinに変換してくれる
地味ですがビックリしました。kotlin化推進に対する誰かの熱き想いを感じます。
static メソッドが使えず、シングルトン(object定義)にした
kotlin初心者としては驚きの事実。staticメソッドが使えない事が判明。
検討の結果、シングルトンにしました。
(Companion Objects 使う方式もありますが、コードがやや冗長になる為、不採用。)
public class DIMock {
object DIMock {
↑こんな感じで、「class」のでなく「object」と定義すると、シングルトンになります。
メソッド呼び出しは、javaとkotlinで違いがあります。
DIMock.INSTANCE.registDICallback(...);
DIMock.registDICallback(...)
java classから参照する場合は「INSTANCE」を経由するんですね。
javadoc -> KDoc になった
/**
* injectをhook{@link Mockito#doAnswer(Answer)}するモック
*/
/**
* injectをhook[Mockito.doAnswer]するモック
*/
kotlinは変数の初期値が無いとビルドエラー
private static AppComponent sMock;
private var sMock: AppComponent? = null
ジェネリクスの「?」は「*」、「Object型」は「Any型」、「new」は不要
private static HashMap<Class<?>, Object> sCustomMocks = new HashMap<>();
private val sCustomMocks = HashMap<Class<*>, Any>()
staticコンストラクターは「init」になる
static {
init {
Class型の取得方法が変わる
sMock = Mockito.mock(AppComponent.class);
sMock = Mockito.mock(AppComponent::class.java)
「::class.java」って書き方が気になったので調べると、 javaのクラス型 Class と、 kotlinのクラス型 KClass があるんですね。
Class<DIMock> jclazz = DIMock.class;
Class<DIMock> jclazz = DIMock.INSTANCE.getClass();
val jclazz : Class<DIMock> = DIMock::class.java
val jclazz : Class<DIMock> = DIMock.javaClass
val kclazz : KClass<DIMock> = DIMock::class
javaではinterface型を引数に取り匿名クラスを渡してた所を、匿名メソッド渡しにできる
public interface DICallback<T> {
void onInject(T listener);
}
public static <T> void registDICallback(Class<T> clazz, DICallback<T> callback) {
...
}
DIMock.registDICallback(MainActivity.class, listener -> { ... });
fun <T> registDICallback(clazz: Class<T>, callback: (listener :T) -> Unit) {
}
DIMock.registDICallback(MainActivity::class.java) { activity :MainActivity ->}
kotlinの場合、javaの様にinterface定義しなくて済みます。
勿論、interface定義して匿名クラスを渡す事も出来ます。
単一メソッドのinterfaceは上記、複数のメソッドなら下記を用いるとコード量が減るかもですね。
DIMock.registDICallback(MainActivity::class.java, object : DIMock.DICallback<MainActivity> {
override fun onInject(listener: MainActivity) {
}
})
kotlinの匿名クラス渡しは、javaのラムダ式より少し煩雑になりますね。。。
kotlinからkotlinへ匿名クラスをラムダ式で渡せないが、kotlinからjavaなら渡せる
・・・と、ここまで書いて気づいた。「あれ、object宣言しなくてもラムダ式で匿名クラス渡せている箇所がある・・・?」
val answer = { invocation : InvocationOnMock ->
...
null
}
...
// 全injectをhookする
for (method in IComponent::class.java.methods) {
...
val hook = Mockito.doAnswer(answer).`when`<AppComponent>(sMock)
...
}
public class Mockito extends ArgumentMatchers {
public static Stubber doAnswer(Answer answer) {
return MOCKITO_CORE.stubber().doAnswer(answer);
}
}
public interface Answer<T> {
T answer(InvocationOnMock invocation) throws Throwable;
}
answer変数は、AndroidStudioのサジェストの機能によると「(InvocationMock)->Nothing?型」でした。
明らかに「Answer型」ではないです。
実は、ここの謎が解けませんでした。kotlinからjavaへの匿名クラス渡しの場合は、javaの文法が適用されるから、ラムダ式が良い様に解釈されてOKって事なのかなぁ。
あまり深追いしません。
匿名メソッドの渡し方が複数ある
ちょっと不思議だったのが、匿名メソッドの渡し方が複数ある事。
どうやらメソッドの引数の最後が匿名メソッドの場合、書き方を変えられるらしい。
// 1. メソッドの引数として渡す
DIMock.registDICallback(xxx, { activity :MainActivity ->})
// 2. 一度「)」で閉じた後に「{}」で括る
DIMock.registDICallback(xxx) { activity :MainActivity ->}
// 3. 匿名メソッド渡しが最後の引数では無い場合、「2」は使えない
DIMock.registDICallback(xxx, { activity :MainActivity ->}, yyy)
javaで定義したsetter/getterに、kotlinからプロパティっぽくアクセスできる
boolean accessible = field.isAccessible();
field.setAccessible(true);
val accessible = field.isAccessible
field.isAccessible = true
Map.put が Map[] で表現できる
sCustomMocks.put(clazz, mock);
sCustomMocks[clazz] = mock
変換後のソースコード
変更前のソースは「こちら」。
object DIMock {
// original component
private val sOriginal = DI.injector()
/**
* injectをhook[Mockito.doAnswer]するモック
*/
private var sMock: AppComponent? = null
private val sCustomMocks = HashMap<Class<*>, Any>()
private val sCallbacks = HashMap<Class<*>, ArrayList<(listener :Any) -> Unit>>()
/**
* DaggerのDIの監視を開始する。
* DIが実行された後、モックに置き換えたい場合は[DIMock.registDICallback] でコールバックを受け取り
* 個別にfieldに代入する、とか、[DIMock.replaceMockAll]を実行すること。
*/
init {
// injectをhook(Mockito.doAnswer)する為の設定をする
// AppComponentの実態クラスは、Daggerによりfinal宣言されているのでspyできない。
sMock = Mockito.mock(AppComponent::class.java)
val answer = { invocation : InvocationOnMock ->
// injectが呼ばれた際に本コールバックが発生する
/**
* 現状、[IComponent.inject] のメソッドは、
* 引数が1つしかないので 0:固定値 とする。
*/
val arg : Any = invocation.getArgument(0)
// originalのinjectを実行する
callInject(sOriginal, arg.javaClass, arg)
// テスト対象のクラスにコールバックする
val list = sCallbacks[arg.javaClass]
if (list != null) {
for (callback in list) {
callback(arg)
}
}
null
}
// 全injectをhookする
for (method in IComponent::class.java.methods) {
/**
* [IComponent.inject] を一回呼んで[Mockito]に登録します。
*
* doAnswer().whien.inject()
* [Mockito.doAnswer]
* [Mockito.when]
*
* このタイミングで[IComponent.inject]をcallしても実際のメソッドが呼ばれる訳ではなく、
* [Mockito]に登録されるだけとなります。
*
* 実際のActivity classなどでDI.injector().inject(this)がコールされたらanserが呼び出されます。
* [DI.injector]
* [IComponent.inject]
*/
val hook = Mockito.doAnswer(answer).`when`<AppComponent>(sMock)
val paramType = method.parameterTypes[0]
callInject(hook, paramType, ArgumentMatchers.any(paramType))
}
DI.setAppComponent(sMock)
}
/**
* injectを実行する
*/
private fun <T> callInject(component: AppComponent, clazz: Class<T>, arg: Any?) {
try {
component.javaClass.getMethod("inject", clazz).invoke(component, arg)
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* テストしたいクラスのinjectをhookするコールバックを登録する
* @param clazz テスト対象のクラス
* @param callback コールバック
*/
fun <T> registDICallback(clazz: Class<T>, callback: (listener :T) -> Unit) {
var list: ArrayList<(listener :T) -> Unit>? = sCallbacks[clazz] as ArrayList<(listener :T) -> Unit>?
if (list == null) {
list = ArrayList()
sCallbacks[clazz] = list as ArrayList<(listener :Any) -> Unit>
}
list.add(callback)
}
/**
* Inject対象のインスタンスのインジェクト済みインスタンスを全て[Mockito.mock]で置き換える
* [DIMock.registCustomMock]で登録済みの場合は、そちらを優先する。
* @param target
*/
fun replaceMockAll(target: Any) {
for (field in target.javaClass.declaredFields) {
if (field.getAnnotation(Inject::class.java) == null) {
continue
}
val accessible = field.isAccessible
try {
field.isAccessible = true
val type = field.type
var mock: Any? = sCustomMocks[type]
if (mock == null) {
mock = Mockito.mock(type)
}
field.set(target, mock)
} catch (e: Exception) {
e.printStackTrace()
} finally {
field.isAccessible = accessible
}
}
}
/**
* モックをカスタムする場合は、事前に登録する
* @param clazz mock化するクラス型
* @param mock mockのインスタンス。[Mockito.mock]を使って作成しても良いし
* テスト用スタブ(自作)を指定してもよい。
*/
fun registCustomMock(clazz: Class<*>, mock: Any) {
sCustomMocks[clazz] = mock
}
}
kotlinのソースとして最適かと言われると、正直、自信が無いのが本音ではあります。
今後、継続的にレベルアップ&ブラッシュアップしたいと思っています。
いかがでしたでしょうか
何かのご参考になれば幸いです。