-
Notifications
You must be signed in to change notification settings - Fork 260
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
Kotlin: Support assertThat(foo) { isNotNull() isNotEqualTo(bar) ... }
#572
Comments
I'm reliably informed that the implementation would be something like:
|
Yep, that'll work! I also flip the parameter to a receiver, but that requires one extension per subject type instead of the |
@JakeWharton Is this what you mean? fun assertThat(string: String, assertions: StringSubject.() -> Unit) {
assertThat(string).run { assertions() }
} This would allow assertThat("hello") {
isNotEmpty()
isEqualTo("hello")
} |
No. The parameter becomes a receiver of the extension. fun String.assertThat(assertions: StringSubject.() -> Unit) = assertThat(this).assertions() |
Got it. Then it reads differently, though. But that's a matter of taste I guess. |
@JakeWharton Would it then read as follows? Does that look better or worse than the explicit "hello".assertThat {
isNotEmpty()
isEqualTo("hello")
} |
Well the input is rarely so trivial. getApi()
.makeSomeCall()
.performSomeAction()
.assertThat {
isNotEmpty()
isEqualTo("hello")
} |
For ease of searching: I'm told that, in addition to https://kotlinlang.org/docs/reference/scope-functions.html#apply Of those existing options, it seems like For purposes of this issue, though, the real choice is the one being discussed above between |
@JakeWharton sure. So I guess the question is which of these seems better, and why?
or
The latter establishes the context of assertion right at the start, but the former maybe allows you to attach the assertion more directly to the clause that names the value ("some action"). Which is more discoverable, harder to get wrong? Which seems more Kotlin-idiomatic? |
@cpovirk If you're talking about using a scope function on the result of calling
Finally, the difference between @JakeWharton please check me as I'm learning this stuff: did I get that right? |
Interesting, thanks. It sounds like assertThat(foo).run {
doesNotContain("foo")
containsAtLeast("bar", "baz")
}.inOrder() But maybe that's so weird that no one would do it :) Probably the takeaway is that (The other takeaway might be that, although users have options already, it's not necessarily obvious which is best, especially given the (remote) possibility of weird usages. That might make this feature slightly more useful than I'd initially realized.) |
I am interested in thoughts from Jake (and other Kotlin users) on what is most idiomatic, but I also want to highlight his point from earlier: The I don't think this rules out the |
Oh yes, in the case of chainable assertions. |
I think the problem of |
I guess someone could try... assertThat(foos).run {
containsAllIn(x)
containsAllIn(y)
}.inOrder() ...and expect |
Yeah, I think I was overrating the difference -- probably because of that existing bias that I mentioned :) There's perhaps still something to be said for the idea that Hmm, and I wonder if Also: No one would be crazy enough to define |
Thinking a little more about
|
In general, extension methods are considered the most idiomatic in Kotlin due to their discoverability. You never need to know where a function lives, because the IDE will suggest all applicable ones on the target type. This is also a boon when you already have one function imported. If you import the core Historically I believe I've named my extensions using just user.name.assert {
isNotEmpty()
isEqualTo("Jake")
} I don't find the English argument as a super strong influencer because it basically mandates you have a named local. Once you replace it with a method call (especially one with arguments) the sentence immediately breaks apart. val box = Box(2, 2, 2, Red)
val scene = buildScene {
putShape(Point(10, 10, 10), box)
}
assertThat(scene.viewport(Point(0, 0, 0), Point(1, 1, 1))) {
containsShape(box)
} Now you have to start inserting words to try and contort it back to English: "Assert that scene [with a] viewport [from] point 0,0,0 [facing] point 1,1,1 contains shape box". This is because we don't try to maintain English readability in the regular API that we're interacting with. And while it may make sense to pull out a local for a single assertion, if I want to check multiple viewports in this test function I would need to start getting creating with names. Is that measurably more readable than scene.viewport(Point(0, 0, 0), Point(1, 1, 1)).assert {
containsShape(box)
} due to trying to maintain English API order? I actually have no opinion on the English-ness of the API, but the import argument above I think is my primary one. |
I have some concerns about making this method that discoverable. If it's an extension method, and your IDE always suggests it, then That may depend somewhat on IDE configuration, but there are a lot of setups out there. |
That would mean Truth would have to be on your non-test classpath which seems like a larger problem. In a test context, though, you can invoke assertions on any object at any time and that seems like it's working as intended. |
(Thanks, this is all very helpful!) At least in my testing with our normal Google build setup, IntelliJ doesn't care that a Truth subject is on the test classpath only: As far as IntelliJ is concerned, there seems to be only one classpath for the prod and test code together. Now, if I actually tried to run the test through the build system, then I would get an error for the missing dependency, since the prod target doesn't depend on the Truth subject. But as far as autocomplete goes, I still see That said:
|
One thing that I don't recall discussing yet here: We might be able to run all the assertions in the block, even if some of them fail, by accumulating their results That said, users may be surprised by such behavior. And it can go sour if one of the assertion throws a straight-up exception (rather than calling into Truth's failure reporting) -- though we could catch it. It could be especially weird if generalized for Still, we should give this at least a passing thought. |
I'm comfortable with recommending I'd humbly suggest that Truth's target users should be mixed-language (J + K) projects. I know extension methods are kotliny, but I think the bar should be a bit higher for justifying a split Truth idiom across the two languages. Even if this means that pure-Kotlin projects have a reason to prefer some other library. Truth being English-grammatical was one of its founding value propositions and I still believe in it. A family member just asked me "what's that new garage code again?" and stood ready to validate my answer. I could have responded, "I claim that equals the 1234 the garage code." (JUnit) IDEA can offer autocomplete suggestions that rewrite what you already typed (like [Note: any of these garage assertions would of course have failed. I'm not dumb enough to use 1234 as my garage code! Who do you take me for? It's 5873.] |
I'm on the side against making the assertion an extension function on the thing being asserted about. I like the distinction between the assertion context and a normal "method" on an object. But I'm coming around to the feeling that we may want to consider a slightly different approach to the "sentence" construction for multiple assertions on a single value. The assertThat(foo) {
isNotNull()
isNotEqualTo(bar)
isSomeOtherThing()
} "Assert that foo is condition" is great for one thing, but for multiple conditions I wonder if using "it" would be better: assertAbout(foo).that {
it.isNotNull()
it.isNotEqualTo(bar)
it.isSomeOtherThing()
} Nice:
Awkward:
|
I don't think we'd want Given that, I suspect that any new methods are going to have to go on Overall, I'm still comfortable with the original proposal. But if we can do better, that's great. It just may be tricky to make the phrasing work, especially when we'd ideally want the result to be "better" than the standard To be fair, we just heard anecdotally from a former team member that new developers, perhaps especially those fresh out of school, typically have some trouble distinguishing |
Thinking further, I now think the right idiom is: long.expression().let {
assertThat(it).isNotNull()
assertThat(it).isNotEqualTo(bar)
assertThat(it).isSomeOtherThing()
} That is very close to what I suggested above about using "it", and uses only standard Kotlin, requiring no Truth API changes at all. There is a small risk of someone dangling an assertion modifier off the end of the |
If Truth has no special feature for this, then I think most kotlin-familiar users will naturally do Chris's original recipe. |
Should we simply document those two strategies ( I don't see a strong argument for a new API. |
Still no plans here, but I wanted to mention something that I was reminded of in a couple other conversations: Whether we do something here or not, we will eventually want to think about the implications for static analysis (CheckReturnValue, TruthIncompatibleType, and others (probably mostly those checks that are specific to Truth)). |
In a similar internal discussion, a colleague points out that an advantage to the long.expression().let {
assertThat(it.foo).isEqualTo(foo)
assertThat(it.bar).isEqualTo(bar)
} Then we're back to hand-wringing over which scope function to use :) It's not like Kotlin needs yet another such function, but I do wish at times like this that there were one that returns |
Additional note: the val goodName = some().longish().expression()
assertThat(goodName).isCondition()
assertThat(goodName.property).isNotOtherCondition() |
It's already possible to write nearly that by using
apply
:Might it be worth a shortcut? Anecdotes welcome.
The text was updated successfully, but these errors were encountered: