-
-
Notifications
You must be signed in to change notification settings - Fork 284
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
RFC: Generics, functions with multiple return values, and Expect(err).NotTo(HaveOccured())
#628
Comments
I've had a few discussions here and there about what generics + Gomega might look like. On the whole it hasn't looked like there are obvious places where the current generics implementation could add much value (without severely hampering flexibility)... ...but I sure do think you're on to something here. This usecase makes a ton of sense and would delete a lot of boilerplate out there. I'm a fan, and I don't think the issue around passing a message is a dealbreaker (the stack trace should be helpful enough to most users). Gomega formally only supports the current and previous release of Go, which means generics are a fine thing to make a hard requirement. I'd be happy to pull this in if you want to start noodling on a PR. |
For the case: foo, err := Bar(42)
Expect(err).NotTo(HaveOccured()) // boring boilerplate
// here come the real assertions Gomega will allow you do to this: Expect(Bar(42)).To(Equal("the answer to life the universe and everything")) It will check that there was no error, and also check that the value matches. But this feature is a bit magical, hard to reason about, and maybe not that widely known. It also doesn't work for three or more values: foo, baz, err := Bar(42)
Expect(err).NotTo(HaveOccured()))
Expect(foo).To(Equal("the answer to life the universe and everything"))
Expect(baz).To(Equal("something else")) What I'd really like to do is: Expect(Bar(42)).To(Equal(
"the answer to life the universe and everything",
"something else",
nil,
)) |
@blgm my RFC here is tackling a different use case, that I admittedly not clearly delineated: you are correct for the common simple cases, where you have a clear expectation of the value. Often time, you might not, but instead the foo return value might be something that you don't really know much about, but instead of feeding it next into something else. commit := Successful(worktree.Commit("check-in", commitopts))
Expect(repo.CreateTag("v1.2.3", commit, nil)).Error().NotTo(HaveOccured()) Here, there are no specific expectations about the returned |
@thediveo, I've definitely used the pattern of defining a generic func must[A any](a A, err error) A {
Expect(err).WithOffset(1).NotTo(HaveOccurred())
return a
} Which is essentially the same as the
I'm genuinely conflicted as to how to move forwards with this. My main fear being that once something has been added to Gomega, it's very hard to get rid of. |
A couple of thoughts, perhaps to allay (corroborate?) some of these concerns from my point of view: On restricting to test code: While it's true that this pattern doesn't need to belong in tests I do think it makes a fair bit of sense for Gomega to include helpers like this out of the box because they are expressing Gomega's opinion on how to consistently handle all errors: pass them through On Go's philosophy: lots could be said here. On the one hand - I've never been too hung up on Ginkgo and Gomega abiding by Go's philosophies. On the other, and perhaps more compelling: that link to the try-catch conversation on the Go 2.0 thread shows just how poorly some of Go's philosophy has translated in practice. Developers routinely don't think about foo, _ := Bar(42)
// do stuff with foo
On the ugliness of On Go 2.0: I suspect that the advent of Go 2.0 will be an opportunity for a rethink for much of Ginkgo/Gomega. It's all murky to me at this point, but I could imagine it necessitating a Gomega 2.0 (and possibly, , a Ginkgo 3.0). So I wouldn't be too hung up on adding things to Gomega today... the great upheaval that is Go 2.0 will likely have a substantial blast radius. |
Perhaps the thing to do would be to start by defining |
Yet another time where I find myself wishing functions could have properties. Wouldn't it be so much (relatively) cleaner if we could write:
Sadly that's not a thing and I imagine the @thediveo - any thoughts on where all this lands for you? |
foo := IfNot.Successfully(Bar(42)).Report("oh, dang!") I envision that we keep this helper in a separate, optional, experimental "success" package that can be dot-imported only when needed, so not part of the baseline standard DSL parts import. |
😂 lol sounds good :) |
Over on the Go issue tracker our beloved topic of better error handling washed another nugget to our shores: a module called "must" that warns its users that it should be used with high caution and for most benefit in unit tests and (that's plain "testing") and code where writing speed is of much more importance than extensive error handling. The latter I often jokingly refer to as "write-once code". Alas, basically same multi-return value handling using Generics, with the well-known limitations. Its design appears to me to be similar to Ginkgo's and Gomega's in throwing specific panics and then catching them elsewhere and understanding what they mean. |
@onsi, in response to this comment, it is possible for functions to have properties in Go as long as they are named types, and it's done in the Go standard library. I'm not saying that this approach should be used, but it's interesting to know it can be done. |
I think there's a caveat at least regarding onsi's example: Successful needs to be a variable and thus means it looses its variablity. |
I've always wondered when writing test specs if there might be a (semi) elegant way to deal with functions returning multiple values and asserting there wasn't any error in a compact way. Now, this is not throwing "the dead cat" of Go error handling proposals "onto the table".
Especially when there are several sequential steps (calls) the error handling really becomes (IMHO) distracting.
While there is unfortunatly (yet?) no way in Go Generics to deal with a variable number of return values, there's a known workaround for the most common cases for two, three, etc return values. For instance in case of functions returning one value plus an error:
The spec now compacts into:
There would probably be variants for
Successfully2
and probably 3 return values.Due to the current limitations of how Go plumbs multiple return values into function calls, I don't see a way to pass a message, except by providing an error message type that implements receivers named
Successfully
, etc.Feedback highly appreciated, more so on the (hah!) generic question on introducing generics into the gomega godebase?
The text was updated successfully, but these errors were encountered: