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()を定義します。
本来はPersonかnullが返る可能性があります。
このテストを書いてみます。
private fun getPerson(): Person? { // emulate searching from DB return Person( age = 40, name = "Dad" ) } data class Person( val age: Int, val name: String )
まずNullableのPersonが isNotNull() で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 != nullでactualが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
サンプルコードは下記にあげました。
おわり。