Skip to content

Add Kotlin contracts to exposed Kotlin API #1866

@JLLeitschuh

Description

@JLLeitschuh

Foreward

First off, I want to thank the Junit 5 team from being so willing to officially support Kotlin as a first-class citizen in the Junit 5 library. It has been absolutely wonderful being able to use my own contributions in all of my Kotlin projects.

Feature Request

I believe that this API can be further enhanced with the new Kotlin 1.3 feature, Contracts.

Contracts are making guarantees to the compiler that various methods have certain characteristics.

Here's an example from the Kotlin Std-Lib:

/**
 * Throws an [IllegalStateException] if the [value] is null. Otherwise
 * returns the not null value.
 *
 * @sample samples.misc.Preconditions.failCheckWithLazyMessage
 */
@kotlin.internal.InlineOnly
public inline fun <T : Any> checkNotNull(value: T?): T {
    contract {
        returns() implies (value != null)
    }
    return checkNotNull(value) { "Required value was null." }
}

Before Kotlin Contracts, the following code wouldn't have compiled:

fun validateString(aString: String?): String {
    checkNotNull(aString)
    return aString
}

I believe that JUnit 5 has a few places where these contracts would be valuable.

Examples

assertNotNull

@ExperimentalContracts
fun <T: Any> assertNonNull(actual: T?, message: String): T {
    contract {
        returns() implies (actual != null)
    }
    Assertions.assertNotNull(actual, message)
    return actual!!
}

The above would allow something like this:

val exception = assertThrows<IllegalStateException { /** whatever **/}
val message = exception.message
assertNotNull(message)
assertTrue(message.contains("some expected substring")) 

Alternatively, it would also allow for this sort of use case:

val message = assertNotNull(exception.message)

assertThrows / assertDoesNotThrow

Since the callable passed to assertThrows is only ever called once, we can expose that in the contract.

@ExperimentalContracts
inline fun <reified T : Throwable> assertThrows(noinline message: () -> String, noinline executable: () -> Unit): T {
    contract {
        callsInPlace(executable, InvocationKind.EXACTLY_ONCE)
    }
    return Assertions.assertThrows(T::class.java, Executable(executable), Supplier(message))
}

Similar

This would for something like this:

val something: Int
val somethingElse: String
assertDoesNotThrow {
    something = somethingThatDoesntThrow()
    somethingElse = gettingSomethingElse()
}

Caveats

Kotlin Contracts are only supported in Kotlin 1.3 and higher.
This would require a discussion regarding what version of Kotlin the Junit 5 team want's to officially support.

Deliverables

  • Add Kotlin method assertNotNull
  • Add Contracts to Kotlin method assertNotNull
  • Add Contracts to Kotlin method assertThrows
  • Add Contracts to Kotlin method assertDoesNotThrow

Metadata

Metadata

Assignees

Projects

Status

Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions