Hi all, I was wondering whether it would make sens...
# kotest
d
Hi all, I was wondering whether it would make sense to support comparing value objects to their underlying values, e.g.
Copy code
@JvmInline
value class Wrapper(value: Int)

Wrapper(1) shouldBe 1
Currently this throws an exception because they have different types. If
shouldBe
is not appropriate (because it would also indicate that
1 shouldBe Wrapper(1)
, maybe a custom extension method such as
shouldHaveValue
or something? Of course, in this case you can easily do
Wrapper(1).value shouldBe 1
a
shouldHaveValue
makes sense to me
d
The only annoying thing is that there is no way during compile time to differentiate a value object from any other object, maybe if it would compile to something with a marker interface like:
Copy code
interface Marker<T> {
    val value: T
}

@JvmInline
value class Wrapper(override val value: Int) : Marker<Int>

infix fun <T> Marker<T>.shouldHaveValue(expected: T) = this.value shouldBe expected

Wrapper(1) shouldHaveValue 1
j
If you dont want an interface, you could use reflection
Copy code
@JvmInline
value class Foo(val value: Int)

infix fun Any.shouldHaveValue(expected: Any?) {
    require(this::class.isValue) { "Not a value class" }
    val actual = if (expected?.let { it::class.isValue } == true) {
        this
    } else {
        this::class.declaredMemberProperties.single().call(this)
    }
    actual shouldBe expected
}

fun main() {
    val foo = Foo(1)
    foo shouldHaveValue 1
    foo shouldHaveValue Foo(1)
}
Or
Copy code
infix fun Any.shouldHaveValue(expected: Any) {
    require(this::class.isValue) { "Not a value class" }
    require(!expected::class.isValue) { "A value class" }
    this::class.declaredMemberProperties.single().call(this) shouldBe expected
}
depending on if you want only
Copy code
val foo = Foo(1)
foo shouldHaveValue 1
and not the following to work
Copy code
foo shouldHaveValue Foo(1)
Also
Copy code
infix fun Any.shouldHaveValue(expected: Any) {
    valueClassValueOrInstance(this) shouldBe valueClassValueOrInstance(expected)
}

private fun Any?.valueClassValueOrInstance(x: Any): Any? {
    return when {
        this != null && this::class.isValue -> this::class.declaredMemberProperties.single().call(this)
        else -> this
    }
}
If you want both previous values and the other way around
Copy code
Foo(1) shouldHaveValue 1
a
very nice
e
In what scenario would you use this? Wouldn't you normally have some
fun foo(): Wrapper
, in which case, wouldn't you want to assert that
foo() shouldBe Wrapper(1)
? Asserting with
shouldHaveValue
would let you swap to another Int-wrapper without the test failing 🤔
Besides, the reflection suggestion would only work on JVM.
👍 1
d
@Emil Kantis you're probably right, the wrapper would usually be something returned and you might want to type check as well as value check, otherwise, it could start returning some other wrapper type and that might nog be what you want. I guess, and this is something we do for some value types already, you can always add your own extension functions for it. So we have a money type with an amount and currency and have e.g.
shouldHaveAmount
for it.