-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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 ErrorAssertionFunc creators #1556
base: master
Are you sure you want to change the base?
Conversation
This is quite a terse issue, but I actually think I know what you mean because I've done it. I'd also like some high-order functions to create assertion functions for ErrorIs and ErrorAs which match the signature of Error and NoError. Example: package kata_test
import (
"testing"
"github.com/brackendawson/kata"
"github.com/stretchr/testify/assert"
)
func TestIfy(t *testing.T) {
for name, test := range map[string]struct {
expectErr func(assert.TestingT, error, ...interface{}) bool
}{
"should work": {
expectErr: assert.NoError,
},
"should return any error": {
expectErr: assert.Error,
},
"should return a specific error": {
expectErr: errorIs(kata.SmellyError),
},
} {
t.Run(name, func(t *testing.T) {
err := kata.Funk()
test.expectErr(t, err)
})
}
}
// errorIs could be part of testify, same for errorAs
func errorIs(target error) func(assert.TestingT, error, ...interface{}) bool {
return func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool {
return assert.ErrorIs(t, err, target, msgAndArgs...)
}
} PS: the function returned by errorIs needs to call t.Helper() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this needs an example, somewhere, perhaps:
func TestIfy(t *testing.T) {
for name, test := range map[string]struct {
expectErr assert.ErrorAssertionFunc
}{
"returns no error": {
expectErr: assert.NoError,
},
"returns error wrapping io.EOF": {
expectErr: assert.ErrorIsFor(io.EOF),
},
"returns error wrapping fs.PathError": {
expectErr: assert.ErrorAsFor(&fs.PathError{}),
},
} {
t.Run(name, func(t *testing.T) {
err := Do()
test.expectErr(t, err)
})
}
}
In practice, for the ErrorAs case I've also wanted to make assertions against the error's fields, can ErrorAsFor
have a function argument for this?
func TestIfy(t *testing.T) {
for name, test := range map[string]struct {
expectErr assert.ErrorAssertionFunc
}{
"returns error wrapping fs.PathError": {
expectErr: assert.ErrorAsFor(func(t assert.TestingT, err fs.PathError) { // testify can infer the type from the function signature
assert.Equal(t, "filename.txt", err.Path)
}),
},
} {
t.Run(name, func(t *testing.T) {
err := Do()
test.expectErr(t, err)
})
}
}
assert/assertions.go
Outdated
@@ -48,6 +48,20 @@ type ErrorAssertionFunc func(TestingT, error, ...interface{}) bool | |||
// Comparison is a custom function that returns true on success and false on failure | |||
type Comparison func() (success bool) | |||
|
|||
// CreateErrorIsAssertionFunc returns an [ErrorAssertionFunc] which tests if the error is a specific value. | |||
func CreateErrorIsAssertionFunc(expectedError error) ErrorAssertionFunc { | |||
return func(t TestingT, err error, i ...interface{}) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function needs to be a helper, ie it should call t.Helper()
Also:
return func(t TestingT, err error, i ...interface{}) bool { | |
return func(t TestingT, err error, msgAndArgs ...interface{}) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can't call t.Helper, there is no helper in this interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add this:
if h, ok := t.(tHelper); ok {
h.Helper()
}
assert/assertions.go
Outdated
@@ -48,6 +48,20 @@ type ErrorAssertionFunc func(TestingT, error, ...interface{}) bool | |||
// Comparison is a custom function that returns true on success and false on failure | |||
type Comparison func() (success bool) | |||
|
|||
// CreateErrorIsAssertionFunc returns an [ErrorAssertionFunc] which tests if the error is a specific value. | |||
func CreateErrorIsAssertionFunc(expectedError error) ErrorAssertionFunc { | |||
return func(t TestingT, err error, i ...interface{}) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add this:
if h, ok := t.(tHelper); ok {
h.Helper()
}
e4ac99d
to
9262a71
Compare
require/requirements.go
Outdated
// ErrorAsFunc returns an [ErrorAssertionFunc] which tests if the any error in err's tree matches target and if so, assigns it to target. | ||
// The returned function panics if target is not a non-nil pointer to either a type that implements error, or to any interface type. | ||
func ErrorAsFor(expectedInterface interface{}) ErrorAssertionFunc { | ||
return func(t TestingT, err error, i ...interface{}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename i
to msgAndArgs
.
require/requirements.go
Outdated
@@ -26,4 +26,27 @@ type BoolAssertionFunc func(TestingT, bool, ...interface{}) | |||
// for table driven tests. | |||
type ErrorAssertionFunc func(TestingT, error, ...interface{}) | |||
|
|||
// ErrorIsFunc returns an [ErrorAssertionFunc] which tests if the error wraps target. | |||
func ErrorIsFor(expectedError error) ErrorAssertionFunc { | |||
return func(t TestingT, err error, i ...interface{}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename i
to msgAndArgs
.
I'm not convinced by the need for |
I agree with the type ErrNotOKStatusCode struct{
StatusCode int
} in which case if I then want to use this error in a higher up function I'd test if it is the correct interface. |
fd08514
to
519f696
Compare
I think // ErrorAsFor returns an [ErrorAssertionFunc] which tests if any error in err's tree matches target in the given fn
// with signature func(t TestingT, target <error_type>) and if so, calls fn with target set to that error.
func ErrorAsFor(fn interface{}) ErrorAssertionFunc func TestIfy(t *testing.T) {
for name, test := range map[string]struct {
expectErr assert.ErrorAssertionFunc
}{
"returns error wrapping fs.PathError": {
expectErr: assert.ErrorAsFor(func(t assert.TestingT, err fs.PathError) {
assert.Equal(t, "filename.txt", err.Path)
}),
},
} {
t.Run(name, func(t *testing.T) {
err := Do()
test.expectErr(t, err)
})
}
} See mock.MatchedBy for how to get the type of an argument to a function. This would need reflection. This also gets rid of the common panic because the user can't call it without using a pointer, which causes people to sometimes raise issues about |
I do not exactly know what you mean, I have added a simple type assertion function in the fixup commit. But I'm not sure that is what you mean. |
@brackendawson Here is a rewrite of your example of ErrorAsFor that I find much more readable (it doesn't require the reader to know the special function func TestIfy(t *testing.T) {
for name, test := range map[string]struct {
expectErr require.ErrorAssertionFunc
}{
"returns error wrapping fs.PathError": {
expectErr: func(t require.TestingT, err error, _ ...interface{}) {
var errPath *fs.PathError
require.ErrorAs(t, err, &errPath)
require.Equal(t, "filename.txt", errPath.Path)
},
},
} {
t.Run(name, func(t *testing.T) {
err := Do()
test.expectErr(t, err)
})
}
} On the Go Playground. |
3f0a0fc
to
b44a5f3
Compare
Yes, that's a better approach. Let's use this PR to add ErrorIsFor, because this function increases readability of people's tests. We can do ErrorAsFor later if there is a good API design. The implementation here does not encourage good testing practice because it can only be used to check that an |
To clean up table driven test with often repeated functions we can add a simple function creator that generates a errorAssertionFunction
b44a5f3
to
1b432fb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Summary
To clean up table driven test with often repeated functions we can add a simple function creator that generates a
ErrorAssertionFunc
.Changes
Motivation
errorAssertionFunc: require.CreateErrorIsAssertionFunc(testedpkg.ExpectedError)