Skip to content

Commit

Permalink
Fix information printed by KProperty0<T>.shouldHaveValue (#3908) (#3921)
Browse files Browse the repository at this point in the history
This PR resolves #3908

* implementation now only returns matcher instead of calling shouldBe
and catching exception, so that it also works with assertSoftly
* extension function is now also mentioned in the core documentation
* added missing package to properties.kt

Co-authored-by: Sam <sam@sksamuel.com>
  • Loading branch information
gianninia and sksamuel committed Mar 10, 2024
1 parent 68a6a28 commit a0c7950
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 32 deletions.
1 change: 1 addition & 0 deletions documentation/docs/assertions/core.md
Expand Up @@ -13,6 +13,7 @@ Matchers provided by the `kotest-assertions-core` module.
| General | |
|-----------------------------------------|--------------------------------------------------------------------------------------------------|
| `obj.shouldBe(other)` | General purpose assertion that the given obj and other are both equal |
| `obj::prop.shouldHaveValue(other)` | General purpose assertion on a property value printing information of the property on failure. |
| `expr.shouldBeTrue()` | Convenience assertion that the expression is true. Equivalent to `expr.shouldBe(true)` |
| `expr.shouldBeFalse()` | Convenience assertion that the expression is false. Equivalent to `expr.shouldBe(false)` |
| `shouldThrow<T> { block }` | General purpose construct that asserts that the block throws a `T` Throwable or a subtype of `T` |
Expand Down
@@ -1,9 +1,3 @@
public final class PropertiesKt {
public static final fun haveValue (Ljava/lang/Object;)Lio/kotest/matchers/Matcher;
public static final fun shouldHaveValue (Lkotlin/reflect/KProperty0;Ljava/lang/Object;)V
public static final fun shouldMatch (Lkotlin/reflect/KProperty0;Lkotlin/jvm/functions/Function1;)V
}

public final class io/kotest/assertions/nondeterministic/ContinuallyConfiguration {
public synthetic fun <init> (JJLio/kotest/assertions/nondeterministic/DurationFn;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1-UwyO8pc ()J
Expand Down Expand Up @@ -1836,6 +1830,13 @@ public final class io/kotest/matchers/paths/PathsKt {
public static final fun startWithPath (Ljava/nio/file/Path;)Lio/kotest/matchers/Matcher;
}

public final class io/kotest/matchers/properties/PropertiesKt {
public static final fun haveValue (Ljava/lang/Object;)Lio/kotest/matchers/Matcher;
public static final fun shouldHaveValue (Lkotlin/reflect/KProperty0;Ljava/lang/Object;)V
public static final fun shouldMatch (Lkotlin/reflect/KProperty0;Lkotlin/jvm/functions/Function1;)V
public static final fun shouldNotHaveValue (Lkotlin/reflect/KProperty0;Ljava/lang/Object;)V
}

public final class io/kotest/matchers/property/MatchersKt {
public static final fun beImmutable ()Lio/kotest/matchers/Matcher;
public static final fun beMutable ()Lio/kotest/matchers/Matcher;
Expand Down
@@ -1,37 +1,45 @@
package io.kotest.matchers.properties

import io.kotest.assertions.Actual
import io.kotest.assertions.Expected
import io.kotest.assertions.eq.eq
import io.kotest.assertions.intellijFormatError
import io.kotest.assertions.print.print
import io.kotest.assertions.withClue
import io.kotest.matchers.EqualityMatcherResult
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNot
import io.kotest.matchers.shouldNotBe
import kotlin.reflect.KProperty0

/**
* Assert that this property has a specific value. Unlike regular [shouldBe], name of the property will be
* automatically added to the error message
* Asserts that this property has a specific value. Unlike regular [shouldBe], the name of the property
* will be automatically added to the error message
*/
infix fun <T> KProperty0<T>.shouldHaveValue(expected: T) = this should haveValue(expected)

/**
* Asserts that this property does not have a specific value. Unlike regular [shouldNotBe], the name of the
* property will be automatically added to the error message
*/
infix fun <T> KProperty0<T>.shouldHaveValue(t: T) = this should haveValue(t)
infix fun <T> KProperty0<T>.shouldNotHaveValue(expected: T) = this shouldNot haveValue(expected)

fun <T> haveValue(t: T) = object : Matcher<KProperty0<T>> {
fun <T> haveValue(expected: T) = object : Matcher<KProperty0<T>> {
override fun test(value: KProperty0<T>): MatcherResult {
val prependMessage = { "Assertion failed for property '${value.name}'" }
val actual = value.get()
val res = runCatching {
actual shouldBe t
}
return object : MatcherResult {
override fun passed(): Boolean =
eq(actual, expected) == null

override fun failureMessage(): String =
prependMessage() + "\n" + intellijFormatError(Expected(expected.print()), Actual(actual.print()))

return EqualityMatcherResult(
res.isSuccess,
actual,
t,
{
val detailedMessage = res.exceptionOrNull()?.message
"Property '${value.name}' should have value $t\n$detailedMessage"
},
{
val detailedMessage = res.exceptionOrNull()?.message
"Property '${value.name}' should not have value $t\n$detailedMessage"
},
)
override fun negatedFailureMessage(): String =
prependMessage() + "\n${expected.print().value} should not equal ${actual.print().value}"
}
}
}

Expand Down
@@ -1,11 +1,12 @@
package io.kotest.matchers.properties

import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldThrowAny
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldStartWith
import shouldHaveValue

class PropertiesKtTest : FunSpec({

Expand All @@ -16,14 +17,100 @@ class PropertiesKtTest : FunSpec({
test("KProperty0<T>.shouldHaveValue with error message") {
shouldThrowAny {
Foo("1")::a shouldHaveValue "2"
}.message shouldStartWith "Property 'a' should have value 2"
}.message shouldBe
"""
Assertion failed for property 'a'
expected:<"2"> but was:<"1">
""".trimIndent()
}

test("KProperty0<T>.shouldHaveValue with error message should include see-difference formatting") {
test("KProperty0<T>.shouldNotHaveValue happy path") {
Foo("1")::a shouldNotHaveValue "2"
}

test("KProperty0<T>.shouldNotHaveValue with error message") {
shouldThrowAny {
Foo("1")::a shouldHaveValue "2"
}.message shouldContain "expected:<\"2\"> but was:<\"1\">"
Foo("1")::a shouldNotHaveValue "1"
}.message shouldBe
"""
Assertion failed for property 'a'
"1" should not equal "1"
""".trimIndent()
}

test("KProperty0<T>.shouldHaveValue with clue in the error message") {
shouldThrowAny {
withClue("This is a clue") {
Foo("1")::a shouldHaveValue "2"
}
}.message shouldBe
"""
This is a clue
Assertion failed for property 'a'
expected:<"2"> but was:<"1">
""".trimIndent()
}

test("KProperty0<T>.shouldHaveValue with assertSoftly happy path") {
val value = Bar("1", 2, 3)
assertSoftly {
value::foo shouldHaveValue "1"
value::bar shouldHaveValue 2
value::bla shouldHaveValue 3
}
}

test("KProperty0<T>.shouldHaveValue with assertSoftly and error message") {
val value = Bar("1", 2, 3)
shouldThrowAny {
assertSoftly {
value::foo shouldHaveValue "1"
value::bar shouldHaveValue 1
value::bla shouldHaveValue 2
}
}.message.also {
it shouldStartWith "The following 2 assertions failed:"
it shouldContain
"""
1) Assertion failed for property 'bar'
expected:<1> but was:<2>
""".trimIndent()
it shouldContain
"""
2) Assertion failed for property 'bla'
expected:<2> but was:<3>
""".trimIndent()
}
}


test("KProperty0<T>.shouldHaveValue with assertSoftly, clue and error message") {
val value = Bar("1", 2, 3)
shouldThrowAny {
withClue("This is a clue") {
assertSoftly {
value::foo shouldHaveValue "1"
value::bar shouldHaveValue 1
value::bla shouldHaveValue 2
}
}
}.message.also {
it shouldStartWith "The following 2 assertions failed:"
it shouldContain """
1) This is a clue
Assertion failed for property 'bar'
expected:<1> but was:<2>
""".trimIndent()
it shouldContain """
2) This is a clue
Assertion failed for property 'bla'
expected:<2> but was:<3>
""".trimIndent()
}
}

})

data class Foo(val a: String)

data class Bar(val foo: String, val bar: Int, val bla: Int)

0 comments on commit a0c7950

Please sign in to comment.