KotlinのContractsを試す

kotlinのContractsを試してみたメモです。

kotlin 1.3からContractsが利用出来るようになりました。
Contracts DSLで事後条件を定義しておくことで、関数呼び出し後の状態を保証することが出来ます。

下記バージョンで試してみます。

  • kotlin 1.4.20
  • junit 4.13.1
  • assertj 3.18.1

Dependency

gradleを使って試してみます。
今回はテストでContractsを試すので、junitとassertjを追加してます。

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    testCompile "junit:junit:4.4"
    testCompile "org.assertj:assertj-core:3.16.1"
}

Contractsを使用しない場合

疑似的にDBからPersonを取得するgetPerson()を定義します。
本来はPersonnullが返る可能性があります。
このテストを書いてみます。

private fun getPerson(): Person? {
    // emulate searching from DB
    return Person(
        age = 40,
        name = "Dad"
    )
}

data class Person(
    val age: Int,
    val name: String
)

まずNullableのPersonisNotNull() でnot nullであることを確認し、
Personの各propertyの値をテストしています。

@Test
fun getPersonTest() {
    // when not using contract
    val person = getPerson()
    assertThat(person).isNotNull
    assertThat(person!!.age).isEqualTo(40)
    assertThat(person.name).isEqualTo("Dad")
}

テストではNullableのpersonに対してisNotNullでnot nullであることを確認済みですが、

    assertThat(person).isNotNull

次の行ではpersonがnot nullであることを認識してくれません。
!!で強制的にnot nullとしてageにアクセスしてます

    assertThat(person!!.age).isEqualTo(40)

ここでperson.ageでアクセスすることは出来ません。

    // error
    assertThat(person.age).isEqualTo(40)

次の行からはすでに!!でnot nullであることが保証されているため、Person.nameでアクセス出来ます。

    assertThat(person.name).isEqualTo("Dad")

Contractsを使用した場合

Contractsを使用して、スマートキャストが効くようにしてみます。

Contracts定義

下記のようにassertThatNotNullという関数を定義し、Contractsを定義してみます。
Contractsを定義するには@ExperimentalContracts アノテーションを付けます。
contract { }の中にContracts DSLで記述します。
returns()assertThat(actual).isNotNullの実行が成功した場合に保証する条件をimplies以下に記載します。
今回の場合は成功した場合、actual != nullactualがnot nullであることを保証します。

@ExperimentalContracts
fun assertThatNotNull(actual: Any?) {
    contract {
        returns() implies (actual != null)
    }
    assertThat(actual).isNotNull
}

assertThatNotNullを利用して先程と同じテストをしてみます。
assertThatNotNull(person)でテストが成功している場合(assertThat(actual).isNotNullが成功している場合)、 assertThat(person.age).isEqualTo(40)でnot nullとしてperson.ageでアクセス出来ています。

@Test
@ExperimentalContracts
fun getPersonTestUsingContract() {
    val person = getPerson()
    assertThatNotNull(person)
    assertThat(person.age).isEqualTo(40)
    assertThat(person.name).isEqualTo("Dad")
}

今度はnullとなる場合を試してみます。

常にnullが返るgetNullPerson()を定義します。

private fun getNullPerson(): Person? {
    // return always null
    return null
}

personがnot nullであることを期待していますが、
getNullPerson()がnullを返すのでテストとしては失敗しなければなりません。

@Test
@ExperimentalContracts
fun getNullPersonTestUsingContract() {
    val person = getNullPerson()
    // error!
    // assertThatNotNull(person)
    assertThat(person).isNull()
}

実行するとassertThatNotNull(person)AssertionErrorが発生し、テストがfailになりました。

Expecting actual not to be null
java.lang.AssertionError: 
Expecting actual not to be null
    at com.example.kotlin.sandbox.contract.MainTestKt.assertThatNotNull(MainTest.kt:61)
    at com.example.kotlin.sandbox.contract.MainTest.nullPersonTestUsingContract(MainTest.kt:33)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)

 
 

[参考]
https://speakerdeck.com/ntaro/kotlin-contracts-number-m3kt

 
サンプルコードは下記にあげました。

おわり。