概要
Kotlinではクラスに属さないメソッドを定義できます。
例えば以下です。
package sample.ko2ic
fun jstCalendar() = Calendar.getInstance().apply {
timeZone = TimeZone.getTimeZone("Asia/Tokyo")
}
で、以下のコードのようにjstCalendar()を利用している箇所の単体テストをするときにどのようにjstCalendar()をモックにするかという話です。
class Hoge {
fun day() :Int {
val calendar = jstCalendar()
return calendar.get(Calendar.DAY_OF_MONTH)
}
}
なお、Mockito + PowerMockでmock化しようとしてもできません。
Javaクラスになった時にstaticメソッドになるのでPowerMockでできそうですが、生成されるクラス名がわからないのでできないのです。
拡張関数に関しても同様です。
方法
Mockitoでテストするとしたら、2018年4月現在、インターフェイスを変えないようにデフォルト引数を利用するしかないと思われます。(後述しますが、Mockkを使えばできます。)
- まずは、Topレベルの関数用にインターフェイスとその実装を追加し、それをデフォルト引数で渡すようにします。
package sample.ko2ic
interface DateExtension {
fun DateExtension.jstCalendar(): Calendar
}
class DateExtensionImpl : DateExtension {
override fun DateExtension.jstCalendar() = Calendar.getInstance().apply {
timeZone = TimeZone.getTimeZone("Asia/Tokyo")
}
}
fun jstCalendar(implement: DateExtension = DateExtensionImpl()): Calendar {
val ext = fun DateExtension.(): Calendar = jstCalendar()
return ext.invoke(implement)
}
ちなみにfun DateExtension.()
はレシーバ付き関数リテラルという記法です。
DateExtensionがレシーバーで戻り値がCalendar型の関数リテラルです。
このとき呼び出されているjstCalendar()
はDateExtension
インターフェイスのメソッドです。
以下のようにも書けます。DateExtensionにClosure(引数がなく戻り値がCalendar)を返す拡張関数を定義しています。
val ext: DateExtension.() -> Calendar = {
jstCalendar()
}
- 次にjstCalendar()と利用している箇所をテストしやすいように少し変更します。(引数にデフォルト実装をすることでインターフェイスが変わらないようにする)
class Hoge {
fun day(implement: DateExtension = DateExtensionImpl()) :Int {
val ext = fun DateExtension.(): Calendar = jstCalendar()
val calendar = ext.invoke(implement)
return calendar.get(Calendar.DAY_OF_MONTH)
}
}
- 後は単純に単体テストで実装を変えるだけです。
@Test
fun day(){
val now = object : DateExtension {
override fun DateExtension.jstCalendar(): Calendar {
val now = Calendar.getInstance().apply {
clear()
set(2000,Calendar.JANUARY,1)
}
return now
}
}
val target = Domain()
val actual = target.day(now)
Assert.assertEquals(1, actual)
}
Mockk
色々書いたけど、結局Mockkを使えば、実装ファイルを何も変えないで単体テストをできるので便利です。
文字列でクラスを指定できます。
@Test
fun day() {
val target = Domain()
val now = Calendar.getInstance().apply {
clear()
set(2000,Calendar.JANUARY,1)
}
staticMockk("sample.ko2ic.mockk.DateExtensionKt").use {
every {
jstCalendar()
} returns now
assertEquals(1, target.day())
verify {
jstCalendar()
}
}
}
結論
- Kotlin + Mockitoは、拡張関数やトップレベルの関数の単体テストがしづらくなるので気をつけよう。
- Mockkは便利だね。