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

Consider renaming/unifying prop/extracting/suspendCall #521

Closed
JakeWharton opened this issue Mar 19, 2024 · 17 comments · Fixed by #524
Closed

Consider renaming/unifying prop/extracting/suspendCall #521

JakeWharton opened this issue Mar 19, 2024 · 17 comments · Fixed by #524

Comments

@JakeWharton
Copy link
Contributor

JakeWharton commented Mar 19, 2024

prop isn't short for property–at least not in the Kotlin-sense. Its KFunction and lambda-accepting overloads let it operate on more than just Kotlin properties. It may be short for "property" in the general sense, but that's then confusing against the Kotlin concept and abbreviations are generally frowned upon in API signatures.

extracting is prop projected over a collection.

suspendCall is the lambda-accepting prop but suspendable.

These are all the same thing, and it would be cool if they had a unified naming convention.

I don't have any perfect naming proposals yet but I've been sitting on filing this issue for months now so I'm firing it off in the hopes others can help.

I came up with two ideas:

Center around the verb "have":

  • Assert that some user has (a) name (that) is equal to "Alice":

    assertThat(someUser).has(User::name).isEqualTo("Alice")
    
    assertThat(someUser).has("name", { it.name() }).isEqualTo("Alice")
  • Assert that all users each has (a) name (that) is equal to "Alice", "Bob", "Eve":

    assertThat(allUsers)
        .eachHas(User::name)
        .isEqualTo(listOf("Alice", "Bob", "Eve"))

    Is this eachHave? Tempted to bend grammar even if it is for consistency.

  • Assert that some user has (a) DB name (that) is equal to "Alice":

    assertThat(someUser).has("name", { it.dbName() }).isEqualTo("Alice")

    Can we pull this off, where the signature matches? Can the main one be inline with some API changes maybe?

Alternatively, lean into map whose semantics are hopefully known from its available on Iterables, Flows, Rx, other languages, etc.

  • Assert that some user map(ped to a) name (that) is equal to "Alice":

    assertThat(someUser).map(User::name).isEqualTo("Alice")
    
    assertThat(someUser).map("name", { it.name() }).isEqualTo("Alice")
  • Assert that all users map(ped to their) name (that) is equal to "Alice", "Bob", "Eve":

    assertThat(allUsers)
        .map(User::name)
        .isEqualTo(listOf("Alice", "Bob", "Eve"))

    Is this eachHave? Tempted to bend grammar even if it is for consistency.

  • Assert that some user map(ped to a) DB name (that) is equal to "Alice":

    assertThat(someUser).map("name", { it.dbName() }).isEqualTo("Alice")

Thoughts?

(Also whoops cmd+enter too soon)

@grodin
Copy link
Contributor

grodin commented Mar 20, 2024

Just a comment on the proposal for using "have": a possible alternative to has could be having. The idea is that the really natural word for these is probably "with" but that is already well-used kotlin syntax so would be a very poor choice. "Having" is kind of a synonym for "with", so might read more naturally.

It would look like this:

assertThat(someUser).having(User::name).isEqualTo("Alice")

assertThat(someUser).having("name", { it.name() }).isEqualTo("Alice")

Then eachHas/eachHave would be eachHaving.

I'll try to open a PR so more code can be written against it to see what it feels like.

@JakeWharton
Copy link
Contributor Author

JakeWharton commented Mar 20, 2024

Yeah I chose "has" because I thought Truth used it pervasively, but it seems like they use it in quite limited fashion. The ones I use constantly are ThrowableSubject.hasMessageThat and ThrowableSubject.hasCauseThat when asserting on failures. I really thought they had more, but it doesn't seem like it.

Their use of the That-suffix is interesting. I parenthesized it out of my natural language sentence equiavlents, but I suppose it could have been included. It would be nice to choose something that didn't require both a prefix and a suffix, though, and maybe "having" gives us that.

@grodin
Copy link
Contributor

grodin commented Mar 20, 2024

I've just realised, it's not going to be possible to unify prop and suspendCall since prop is an instance method and suspendCall is an extension method, so giving both the same name will mean the suspend fun is shadowed.

If only you could have a suspend fun overriding a non-suspend fun (I understand why that's not possible, I'm just musing.)

@JakeWharton
Copy link
Contributor Author

prop is also an extension. I think its implementation APIs (transform and appendName) are both public so it probably could just be made inline so that it works in both contexts (like assertFailure does).

@grodin
Copy link
Contributor

grodin commented Mar 21, 2024

Would that be an ABI break? Might be worth doing it separately to this if it is.

@JakeWharton
Copy link
Contributor Author

We're already renaming the functions and breaking everyone. The deprecation replace with for both prop and suspendCall can both target the new, single function.

@grodin
Copy link
Contributor

grodin commented Mar 21, 2024

D'oh! Obviously we are!

@evant
Copy link
Collaborator

evant commented Mar 25, 2024

prop isn't short for property–at least not in the Kotlin-sense. Its KFunction and lambda-accepting overloads let it operate on more than just Kotlin properties.

It originally was but became more general shortly after, I agree with the sentiment of this issue.

Yeah I chose "has" because I thought Truth used it pervasively, but it seems like they use it in quite limited fashion. The ones I use constantly are ThrowableSubject.hasMessageThat and ThrowableSubject.hasCauseThat when asserting on failures. I really thought they had more, but it doesn't seem like it.

that's an interesting connection. We do actually have a good number of 'has*' methods which may make this usage more natural.

For reference:

  • assertj calls both the object and collection methods 'extracting'
  • truth calls the object method 'check' and doesn't have a collection equivalent (lmk if this is wrong)

I think its implementation APIs (transform and appendName) are both public so it probably could just be made inline so that it works in both contexts (like assertFailure does).

there was a reason this wasn't done, but I forget why off the top of my head

@evant
Copy link
Collaborator

evant commented Mar 25, 2024

Another note: kotlin itself doesn't unify on a name for their similar concept (let vs map), so while I do strongly agree that prop & suspendCall should be renamed I feel much less strongly that extracting should.

@evant
Copy link
Collaborator

evant commented Mar 25, 2024

Linking #522 (comment) as the same argument might be relevant here, a point against 'has' is that it may sound like an assertion itself instead of a middle step, ex someone might do:

assertThat(someUser).has(User::name)

not realizing it will never fail

edit: This makes me like the 'having' suggestion above a bit more, I feel like it has less of that connotation

@JakeWharton
Copy link
Contributor Author

Indeed Kotlin doesn't use the same name, although I also don't think that's a strong design decision on their part. map is basically forced on them, and let seems more designed within the let/run/with/apply set.

One important difference is that let and map operate directly on the subject whereas the functions here operate on the subject through an Assert wrapper. I think you could make an argument for having the ones here use the same root name(verb?) for the operation, but use something added like an "each" prefix or suffix when operating within an iterable.

@evant
Copy link
Collaborator

evant commented Mar 25, 2024

Also there's a couple of other ways that extracting is different from prop today. It does not take a name to prepend to the failure message, and it has overloads that allow you to extract 2 or 3 values at a time. (the second on is helpful for when you want to be more specific on what you assert on but still use the contains* methods.)

ex:

assertThat(users).extracting(User::name, User::age).containsExactly(
    "Alice" to 22,
    "Bob" to  19
)

@evant
Copy link
Collaborator

evant commented Apr 1, 2024

One thing worth exploring would be to add in addition to/replace extracting with a version of eachHaving that more directly mirrors having
ex:

assertThat(allUsers).eachHaving(User::name).isEqualTo(listOf("Alice", "Bob", "Eve"))

assertThat(allUsers).eachHaving("name") { it.name }.isEqualTo(listOf("Alice", "Bob", "Eve"))

@evant
Copy link
Collaborator

evant commented Apr 17, 2024

Since I haven't seen any other suggestions given and can't think of anything better, I think we should go with the having pattern.

@grodin
Copy link
Contributor

grodin commented Apr 18, 2024

One thing worth exploring would be to add in addition to/replace extracting with a version of eachHaving that more directly mirrors having ex:

assertThat(allUsers).eachHaving(User::name).isEqualTo(listOf("Alice", "Bob", "Eve"))

assertThat(allUsers).eachHaving("name") { it.name }.isEqualTo(listOf("Alice", "Bob", "Eve"))

I hadn't read this properly when you first posted it. I think this is a really good idea. When #524 is merged, I'll get another draft PR opened.

@cpovirk
Copy link

cpovirk commented May 7, 2024

Hi, I came across this when looking at mentions of Truth. I'll offer a few thoughts, but I'm not sure how relevant they'll end up being.

  • Truth doesn't have anything quite like this for individual objects (though we've considered it).
    • check() is used to implement things like this, but it's available only to users who are defining their own Subject subclass (our equivalent to an AssertJ Assert subclass, which of course you don't need an equivalent to): They use it to define, say, a hasName(name: String) function of their own for users to call, with an implementation of check("name").that(actual.name).isEqualTo(name). But we don't have anything that a user can directly use for generic comparisons of this style.
  • Truth does have this for collections: "Fuzzy Truth" assertions look something like assertThat(allUsers).comparingElementsUsing(Correspondence.transforming(User::name, "has name that")).containsExactly("Alice", "Bob", "Even"). That approach has its pros and cons, and I have wondered if we can someday justify a shortcut like what you've outlined above (not to mention better failure messages for it).
  • I'm not sure I'd use the "hasMessageThat" style if we were doing it again. It's very long, and I'm not sure that that length accomplishes much. And while you make a good point about the danger of assertThat(someUser).has(User::name), that's more of a danger when the function accepts an argument (as in your example) and less so for a case like assertThat(someException).message() (or perhaps assertThat(someException).message in Kotlin). (Compare what Androidx did for their Truth extensions.) But this is one of those cases in which my thoughts aren't very relevant to AssertK because your proposed function does accept an argument.
    • We do have a few methods that don't fit fit that style, such as valuesForKey(key). (Jake might also be recalling sadly still-internal extensions like FutureSubject.hasFailureThat().)

@evant
Copy link
Collaborator

evant commented May 7, 2024

Thanks for the thoughts!

check() is used to implement things like this, but it's available only to users who are defining their own Subject subclass (our equivalent to an AssertJ Assert subclass, which of course you don't need an equivalent to

yep I was aware of that, I left out the distinction for simplicity because the function we are discussing here is used both for implementing your own assertions as well as something that can be used directly.

Truth does have this for collections: "Fuzzy Truth" assertions

Yep, I've been trying to figure out for a while how to fill the same gap, as you point that that approach has pros and cons and and don't exactly love the cons of it. I do think the adjustments here do go in the right direction though I'm not sure yet if it covers all the same use-cases well enough.

I'm not sure I'd use the "hasMessageThat" style if we were doing it again.

The current proposal is to use the style: havingMessage() (see https://github.com/willowtreeapps/assertk/pull/537/files). I'm not completely sold on this yet as opposed to the current shorter message() syntax, but does bring some consistency here and #522 correctly points out that it can't be used for all functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants