Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for property based assertion #3908

Closed
gianninia opened this issue Mar 4, 2024 · 7 comments · Fixed by gianninia/kotest#1 or #3921
Closed

Add support for property based assertion #3908

gianninia opened this issue Mar 4, 2024 · 7 comments · Fixed by gianninia/kotest#1 or #3921
Labels
assertions 🔍 Related to the assertion mechanisms within the testing framework. enhancement ✨ Suggestions for adding new features or improving existing ones.

Comments

@gianninia
Copy link
Contributor

gianninia commented Mar 4, 2024

Very often it's required to assert multiple properties of a given object and if the assertion fails, you have no clue on which property the assertion failed, unless of course you use the nice "Clues" feature of Kotest 😄. Writing clues for each property however feels a bit verbose and clunky and it's not type safe if the property is renamed for example.

Here an example to show the problem:

data class Person(val firstName: String, val lastName: String, val age: Int)

val expected = Person("James", "Bond", 56)
val actual = Person("James", "Bond", 52)

withClue("Unexpected firstName") { actual.firstName shouldBe expected.firstName }
withClue("Unexpected lastName") { actual.lastName shouldBe expected.lastName }
withClue("Unexpected age") { actual.age shouldBe expected.age }

Of course, one could simply assert the whole object (especially if it's a data class) like so:

actual shouldBe expected

But what if I have many properties in my class and I don't want to assert all properties because not all are relevant for my test case. Plus, what if I have many properties and only one fails? I would get a huge string generated for each object and it wouldn't be that easy to find which property is different..

So, I tried to find something in the API that could help me with this problem but couldn't find anything, so I ended up creating the following extension function:

/**
 * Extends Kotest's [shouldBe] to work with properties of type [KProperty0],
 * i.e. property references on instances, so that the name of the property
 * can be printed as clue if the assertion of the property's values fail.
 *
 * Example:
 * ```
 * data class MyValue(val prop: Int)
 *
 * val expected = MyValue(1)
 * val passes = MyValue(1)
 * val fails = MyValue(0)
 *
 * // will pass
 * passes::prop shouldGet expected::prop
 * // will fail with message:
 * // Unexpected prop
 * // expected:<1> but was:<0>
 * fails::prop shouldGet expected::prop
 * ```
 */
infix fun <V> KProperty0<V>.shouldGet(expected: KProperty0<V>) {
    withClue("Unexpected $name") {
        get() shouldBe expected.get()
    }
}

As you may have guessed from the code and KDoc comment, this allows me to write the code above as follows and yield the exact same result:

data class Person(val firstName: String, val lastName: String, val age: Int)

val expected = Person("James", "Bond", 56)
val actual = Person("James", "Bond", 52)

actual::firstName shouldGet expected::firstName
actual::lastName shouldGet expected::lastName
actual::age shouldGet expected::age

To make it really nice for bigger classes, one could do something like that:

withClue("Assertion for Person failed") {
    assertSoftly {
        actual::firstName shouldGet expected::firstName
        actual::lastName shouldGet expected::lastName
        actual::age shouldGet expected::age
    }
}

Which brings me to further ideas like having an API where I can pass the actual and expected objects and a list of properties and it generates the clue with the class name like above.

Anyway, this is not necessarily a feature request but just wanted to share this use-case with this forum to check if there is a bigger demand for something like this and let you guys consider adding such concepts to the API in future releases. I'm happy to help with the coding part if required.

@LeoColman LeoColman added enhancement ✨ Suggestions for adding new features or improving existing ones. assertions 🔍 Related to the assertion mechanisms within the testing framework. labels Mar 4, 2024
@Kantis
Copy link
Member

Kantis commented Mar 6, 2024

We do have shouldHaveValue which is essentially the same thing? #3637
Do you agree that it solves the same use case?

Kudos for putting the effort into writing a great issue 💯

@gianninia
Copy link
Contributor Author

gianninia commented Mar 7, 2024

We do have shouldHaveValue which is essentially the same thing? #3637 Do you agree that it solves the same use case?

Kudos for putting the effort into writing a great issue 💯

That's exactly it and a much cleaner API than what I suggested! Thank you very much!! 😃

It's a bit hard to find though (it was at least for me).

I think something that useful deserves at least to be mentioned here https://kotest.io/docs/assertions/core-matchers.html. I would create a specific table with the name of "Properties" for it and place it right after "General". Unless this usage is not that common as I think it is.

I will definitely use it very often! Thanks again!

@gianninia
Copy link
Contributor Author

gianninia commented Mar 7, 2024

OK, I think I've just found a a bug in shouldHaveValue. It doesn't print the property information if it's used inside an assertSoftly block. On the other hand, it seems to print the expected and actual values twice if not within an assertSoftly block.

This is how I could reproduce it:

Wihtout assertSoftly block:

data class Person(val firstName: String, val lastName: String, val age: Int)

val expected = Person("James", "Bond", 56)
val actual = Person("John", "Bond", 52)

actual::firstName shouldHaveValue expected.firstName
actual::lastName shouldHaveValue expected.lastName
actual::age shouldHaveValue expected.age

Prints:

io.kotest.assertions.AssertionFailedError: Property 'firstName' should have value James
expected:<"James"> but was:<"John">
expected:<"James"> but was:<"John">

With assertSoftly block:

data class Person(val firstName: String, val lastName: String, val age: Int)

val expected = Person("James", "Bond", 56)
val actual = Person("John", "Bond", 52)

withClue("Assertion for Person failed") {
    assertSoftly {
        actual::firstName shouldHaveValue expected.firstName
        actual::lastName shouldHaveValue expected.lastName
        actual::age shouldHaveValue expected.age
    }
}

Prints:

io.kotest.assertions.MultiAssertionError: The following 2 assertions failed:
1) Assertion for Person failed
expected:<"James"> but was:<"John">
   at PropertiesKt$haveValue$1.test(properties.kt:19)
2) Assertion for Person failed
expected:<56> but was:<52>
   at PropertiesKt$haveValue$1.test(properties.kt:19)

As you can see the version with the soft block is missing the message Property 'firstName' should have value James.

I had a look at the code and the problem seems to be in the runCatching{ } block in the haveValue() function (Version 5.8.0, file PropertiesKt, line 18). When running in soft mode, no exception is thrown, so the part further down that composes the message with the property details is never reached. I suspect the cause of the duplicated message when not in soft mode has also to do with it but didn't investigate further.

@Kantis
Copy link
Member

Kantis commented Mar 7, 2024

Do you want to contribute a fix? Sounds like you're pretty on top of it already 😄

@gianninia
Copy link
Contributor Author

I was actually considering it while I was writing 😄. I'll give it a try.

@gianninia
Copy link
Contributor Author

Ok, I've prepared a fix now but I'm not able to push it or create a pull request. Do you have to give me permission first?

Can't create a new pull request: Push failed: remote: Permission to kotest/kotest.git denied to gianninia. unable to access 'https://github.com/kotest/kotest.git/': The requested URL returned error: 403

@sksamuel
Copy link
Member

For open source projects, you need to fork the repo, and then you push the branch to your fork. Then github will prompt you to make a PR back from your fork to this repo.

gianninia added a commit to gianninia/kotest that referenced this issue Mar 10, 2024
This PR resolves kotest#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
gianninia added a commit to gianninia/kotest that referenced this issue Mar 10, 2024
Fix information printed by KProperty0<T>.shouldHaveValue (kotest#3908)
@gianninia gianninia reopened this Mar 10, 2024
sksamuel added a commit that referenced this issue Mar 10, 2024
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
assertions 🔍 Related to the assertion mechanisms within the testing framework. enhancement ✨ Suggestions for adding new features or improving existing ones.
Projects
None yet
4 participants