2018年11月10日土曜日

sealed class に共通データを持たせるときは abstract val にする

sealed class Pet を継承した data class Cat と Dog があるとします。 sealed class Pet data class Cat(val name: String, val kind: CatKind) : Pet() data class Dog(val name: String, val kind: DogKind) : Pet() name プロパティは Pet として必須なので Pet class に持たせたいですね。
しかし次のように primary コンストラクタの property paramter として持たせようとするとコンパイルエラーになります。 sealed class Pet(val name: String) data class Cat(name: String, val kind: CatKind) : Pet(name) // コンパイルエラー data class Dog(name: String, val kind: DogKind) : Pet(name) // コンパイルエラー data class の primary コンストラクタは property parameter (val or var) しか持てないからです。 そこで、どうするかというと name を abstract val として Pet に持たせます。 sealed class Pet { abstract val name: String } data class Cat(override val name: String, val kind: CatKind) : Pet() data class Dog(override val name: String, val kind: DogKind) : Pet()


2018年11月4日日曜日

古い Mockito では Kotlin の suspend fun を override してくれないので 2.23.0 以降を使う

追記

mockito 2.23.0 で suspend fun のサポートが入った(Support mocking kotlin suspend functions compiled by Kotlin 1.3 (#1500))と教えていただいたので、試してみました。

2.23.0 の mockito なら以下のテストが成功しました!やったー! // このテストが 2.23.0 の mockito なら成功する! @Test fun test() { runBlocking { val mock = mock(Greeting::class.java).apply { `when`(hello()).thenReturn("Hello Android") } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } もともと試した mockito のバージョンは 2.8.9 です。(なぜこのバージョンかというと iosched がこのバージョンを使っていたからです。特に深い意味はありません。)


以下は mockito 2.8.9 での動作です。

以下のような interface と class があるとします。 interface Greeting { fun hello(): String } class GreetingTextCounter(private val greeting: Greeting) { fun count(): Int { return greeting.hello().length } } Greeting のモックを用意して特定の文字列を hello() で返すようにすれば、GreetingTextCounter.count() のテストができます。 @Test fun test() { val mock = mock(Greeting::class.java).apply { `when`(hello()).thenReturn("Hello Android") } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } ここで Greeting.hello() と GreetingTextCounter.count() を suspend にします。
interface Greeting { suspend fun hello(): String } class GreetingTextCounter(private val greeting: Greeting) { suspend fun count(): Int { return greeting.hello().length } } すると、以下のテストは失敗します。 // このテストは失敗する @Test fun test() { runBlocking { val mock = mock(Greeting::class.java).apply { `when`(hello()).thenReturn("Hello Android") } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } `when`().thenReturn() で hello() のときに "Hello Android" を返すように指定していても、GreetingTextCounter.count() のところで greeting.hello() が null を返してしまい、 java.lang.NullPointerException になります。

以下のように Greeting を実装した object を用意して hello() を override すればテストは成功します。 // このテストは成功する @Test fun test() { runBlocking { val mock = object : Greeting { override suspend fun hello() = "Hello Android" } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } しかし使用しないメソッド以外も override しなければいけないのでよくありません。

そこで Delegation を活用し、使用しないメソッドは Mockito の mock に流すようにします。 // このテストは成功する @Test fun test() { runBlocking { val mock = object : Greeting by mock(Greeting::class.java) { override suspend fun hello() = "Hello Android" } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } これで必要なメソッドだけ override し、テストも通るようになります。