-
Notifications
You must be signed in to change notification settings - Fork 18.7k
Closed
Labels
Milestone
Description
Problem
Currently, there is no general way to test whether a returned error value is what the developer expected:
- The obvious == operator doesn’t work, since *errors.stringError is designed to be unique: proposal: Add constant error #17226 (comment).
- Using errors.Is is also unsatisfactory because the developer must go through extra effort to test both the wrapped error and the Error string, as demonstrated by fmt_test.TestErrorf.
- reflect.DeepEqual is sometimes correct for testing errors, but makes these tests particularly brittle when it comes to wrapped errors.
- Finally, cmp.Equal is a popular third-party package for comparisons, but even its cmpopts.EquateErrors option doesn’t do the right thing here.
Ideally, a developer could write the following test:
func TestFunc(t *testing.T) {
wantErr := fmt.Errorf("my: %w", fs.ErrNotExist)
err := my.Func()
if !errors.Like(err, wantErr) {
t.Fatalf("err %v, want %v", err, wantErr)
}
}Workarounds
Often, we see a helper function inside the test suite that helps compare Error text by dealing with nil errors:
func Error(err error) string {
if err == nil {
return ""
}
return err.Error()
}
func TestFunc(t *testing.T) {
wantErr := fmt.Errorf("my: %w", fs.ErrNotExist)
err := my.Func()
if Error(err) != wantErr {
t.Fatalf("err %v, want %v", err, wantErr)
}
}This is unsatisfying now that we have wrapped errors, because even if the Error strings are equal, that doesn’t mean that any wrapped errors match.
Concerns
- The name of errors.Like may not be obvious. I considered errors.Match, but the errors documentation already uses the word “match” with a slightly different meaning.
- Since the primary use-case for this function is to test errors, and not to handle them, it may not belong in the errors package. We don’t want developers to choose Like when they mean Is. Perhaps it should go in a new errors/errorstest package?
Proposed implementation
The following is a proposed implementation of errors.Like:
// Like reports whether err is equivalent to target.
//
// An error is considered to be equivalent if it is equal to the target.
// It is also equivalent if its Error string is equal to the target’s Error
// and its wrapped error is equivalent to the target’s wrapped error.
func Like(err, target error) bool {
if err == target {
return true
}
if err == nil || target == nil {
return false
}
if utarget := Unwrap(target); utarget != nil {
if err.Error() != target.Error() {
return false
}
return Like(errors.Unwrap(err), utarget)
}
return false
}You can also find an implementation with test cases in the Go Playground: https://play.golang.org/p/qnBbkSbMlLO