Add Must & Mustf - #1945
Conversation
brackendawson
left a comment
There was a problem hiding this comment.
Lots of new concepts to testify here.
- Generics
- Assertions gated by go version build constraints
- Assertions which are only in the require package
- It's also debatable that this isn't an assertion, it's a helper function you can use in test setup.
The precedent has been clear in the past that testify does not try to be a utility package. Is it worth breaking all this new ground for what amounts to a 1-line saving.
Also is "must" the best name? By convention it raises errors to a panic, not a test failure.
It's not a no, but these are some points around which I would need some convincing to include the change.
| // The go.mod go directive is go1.17 so that testify keeps building on old | ||
| // toolchains; a //go:build constraint can only raise the language version on | ||
| // Go 1.21 and later, so that is the lowest version this file can be gated on. | ||
| //go:build go1.21 |
There was a problem hiding this comment.
Probably better to gate this on 1.27 too, like in require.Assertions.Must
There was a problem hiding this comment.
Why so, when 1.21 can run the static functions?
There was a problem hiding this comment.
I think it might be confusing to someone using 1.21-1.26 to find that it's available under require but not available under require.Assertions.
I could probably be sold on not changing this part.
There was a problem hiding this comment.
Maybe add it here just for 1.27, and seeing if there’s a demand for static-only usage in prior.
| // | ||
| // Because the test is stopped with [testing.T.FailNow], Must must be called | ||
| // from the goroutine running the test function. | ||
| func Must[T any](t TestingT, f func() (T, error), msgAndArgs ...interface{}) T { |
There was a problem hiding this comment.
Don't mix any and interface{} in the same function declaration.
|
Thank you for your notes. To your concerns:
I would argue that testify already is a utility package, insofar as it wraps testing.T’s primitives with convenience logic like NoError(). In my own experience this 1-line savings repeats over & over, impeding legibility & making tests clunkier. To your points, though: this sort of thing may be more germane to OpenAPI’s fork. |
|
I think we can probably include this. Should we make an assert version too? I'd say no. This is permissible: func TestIfy(t *testing.T) {
b := bytes.Buffer{}
count, err := b.Write(nil)
assert.NoError(t, err)
assert.Equal(t, count, 0)
}But the return parameter is already occupied and almost always ignored so we just can't do it sensibly. I actually never use the assert package myself, only require. I'm also quite supportive of the OpenAPI fork, it's close to the v2 I've always wanted to write. Any major version bump of a Go module is a fork, even if it's made in the same repo, and Stretchr is not the place to go starting a new fork. |
|
@FGasper hey. Sorry I missed this PR. Indeed a good demo of how generics may help write more concise tests. Will definitely add this to go-openapi/testify/v2 fork (next release if scheduled for ~ end september). Thanks for this great idea. fred |
| // | ||
| // Because the test is stopped with [testing.T.FailNow], Must must be called | ||
| // from the goroutine running the test function. | ||
| func Must[T any](t TestingT, f func() (T, error), msgAndArgs ...interface{}) T { |
There was a problem hiding this comment.
@FGasper I've experimented with a few implementations on my fork since your proposals.
And all were so far a bit disappointing.
We all agree that this feature is all about providing a syntactic sugar to replace
value, err := fn(args...)
require.NoError(t, err)
...by something more concise and that better conveys the intent to use the value.
Your proposal comes a bit short of that as soon as the function takes some parameters:
Yields:
value := require.Must(t, func() (string, error) { return fn(args...) })
...Not much of an improvement, uh?
I have tried yet another way:
func Must[V any](t TestingT, msgAndArgs ...any) func() (V, error) {
....
}And usage looks like:
value := require.Must[int](t)(strconv.Atoi("1"))
...or as a "forward" method:
myTest := require.New(t)
value := myTest.Must[int]()(strconv.Atoi("1"))
...which arguably look better (I can inline the function arguments), but requires an explicit type hint.
go is unable to infer the type parameter directly from the the function. This doesn't build as of go1.27:
value := require.Must(t)(strconv.Atoi("1"))
...A bit disappointing. This kind of mitigated my initial enthusiasm to add this (at least to the fork I am maintaining, which has full support for generics and generic methods).
Further, this helper is decidedly suited for require and doesn't make much sense with assert (or would it be called Should ?). This kind of break the symmetry of these packages: either the "assert" version becomes blocking (or "Must" doesn't make much of sense) or it is not generated there, or it is renamed as Should... In either case, this would need a lot of additional explanations, for overall a saving of perhaps 10-15 characters.
Maybe I missed something - I'd be happy to try alternate ways, but for now I am a bit short of brighter ideas that could make the helper, actually useful. Any thoughts?
|
@fredbi et al.: I remember why I never submitted this idea before. The idea is to mimic samber/lo.Must: That works only as a generic method because it doesn’t require *testing.T as an argument. As a static function it doesn’t work because Go won’t parse static So in 1.27 this is useful where require.Assertions is used. I personally, though, don’t frequently use require.Assertions. I’ll leave this open in case others have ideas, but I’m OK to close this. Sorry, I should have fleshed this out better before opening the PR. |
Moves stretchr#1878 from Monitoring to Adapted, restricted to the forward methods, and starts monitoring stretchr#1945 (a Must helper returning a value when there is no error). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qDqrLvtn2K5uA6RpSoHvR Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
* landed our implementation of "hunkSize" parameterization (reference: stretchr#1878) * won't implement "require.Must": the cost of the asymmetry offset the expected minor conciseness gains (reference: stretchr#1945 Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
Summary
Convenience methods around operations that must succeed.
Changes
require.Mustandrequire.Mustfgeneric functions.These are usable only in Go 1.21+ and 1.27+, respectively. (The function versions would work in 1.18-1.20, but release-tag limitations in those versions prevent that.)
Motivation
These are convenience methods. Usage looks thus:
It’s a bit more ergonomic than:
Thank you!