Skip to content

proposal: errors: add Like for testing that error values appear as expected #49172

@sfllaw

Description

@sfllaw

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

See also

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions