2018年11月10日土曜日

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

sealed class Pet を継承した data class Cat と Dog があるとします。
  1. sealed class Pet  
  2.   
  3. data class Cat(val name: String, val kind: CatKind) : Pet()  
  4. data class Dog(val name: String, val kind: DogKind) : Pet()  
name プロパティは Pet として必須なので Pet class に持たせたいですね。
しかし次のように primary コンストラクタの property paramter として持たせようとするとコンパイルエラーになります。
  1. sealed class Pet(val name: String)  
  2.   
  3. data class Cat(name: String, val kind: CatKind) : Pet(name) // コンパイルエラー  
  4. data class Dog(name: String, val kind: DogKind) : Pet(name) // コンパイルエラー  
data class の primary コンストラクタは property parameter (val or var) しか持てないからです。 そこで、どうするかというと name を abstract val として Pet に持たせます。
  1. sealed class Pet {  
  2.     abstract val name: String  
  3. }  
  4.   
  5. data class Cat(override val name: String, val kind: CatKind) : Pet()  
  6. 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 なら以下のテストが成功しました!やったー!
  1. // このテストが 2.23.0 の mockito なら成功する!  
  2. @Test  
  3. fun test() {  
  4.     runBlocking {  
  5.         val mock = mock(Greeting::class.java).apply {  
  6.             `when`(hello()).thenReturn("Hello Android")  
  7.         }  
  8.   
  9.         val counter = GreetingTextCounter(mock)  
  10.   
  11.         assertThat(counter.count()).isEqualTo(13)  
  12.     }  
  13. }  
もともと試した mockito のバージョンは 2.8.9 です。(なぜこのバージョンかというと iosched がこのバージョンを使っていたからです。特に深い意味はありません。)


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

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

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

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