Repository: stretchr/testify (go)

Issue:
testify `{assert|require}` packages seem to be missing `NotErrorAs` methods.
When using this gem of a library I realized that the `assert` and `require` package seem to be missing a convenience function for `NotErrorAs(...)`.

`{assert|require}.NotErrorIs(...)` does exist so the absence of the `error.As` equivalent feels weird.

If there is a reason for this, which I do not see, I would love to be enlightened, else I would happily create a PR for this (:

Right now one has to do the following steps to check for this (which kinda breaks the feel IMHO):

```Golang
var customErr *package.CustomErr
assert.Error(t, err)
assert.False(t, errors.As(err, &customErr))
```

or similar.

Pull request:
Add assertion for NotErrorAs
## Summary
The library had implementations for ErrorIs, ErrorAs and NotErrorIs but
was missing a NotErrorAs implementation. This PR adds a NotErrorAs
function, which is the inverse of the ErrorAs function.

## Changes
* Adds `NotErrorAs` function and test
* Improves existing tests (`TestErrorIs`, `TestNotErrorIs` and `TestErrorAs`) by comparing assertion result and `testing.T` flag.
* Clarifies documentation for existing `NotErrorIs` function

## Motivation
There's already an inverse of `ErrorIs`, `ErrorAs` should have an inverse as well.

## Related issues
<!-- Put `Closes #XXXX` for each issue number this PR fixes/closes -->
Closes #1066


Patch:
diff --git a/assert/assertion_format.go b/assert/assertion_format.go
index 546fe1f..1906341 100644
--- a/assert/assertion_format.go
+++ b/assert/assertion_format.go
@@ -621,6 +621,15 @@ func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg s
 	return NotEqualValues(t, expected, actual, append([]interface{}{msg}, args...)...)
 }
 
+// NotErrorAsf asserts that none of the errors in err's chain matches target,
+// but if so, sets target to that error value.
+func NotErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) bool {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
+	return NotErrorAs(t, err, target, append([]interface{}{msg}, args...)...)
+}
+
 // NotErrorIsf asserts that none of the errors in err's chain matches target.
 // This is a wrapper for errors.Is.
 func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool {
diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go
index 8504dca..2162908 100644
--- a/assert/assertion_forward.go
+++ b/assert/assertion_forward.go
@@ -1234,6 +1234,24 @@ func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg str
 	return NotEqualf(a.t, expected, actual, msg, args...)
 }
 
+// NotErrorAs asserts that none of the errors in err's chain matches target,
+// but if so, sets target to that error value.
+func (a *Assertions) NotErrorAs(err error, target interface{}, msgAndArgs ...interface{}) bool {
+	if h, ok := a.t.(tHelper); ok {
+		h.Helper()
+	}
+	return NotErrorAs(a.t, err, target, msgAndArgs...)
+}
+
+// NotErrorAsf asserts that none of the errors in err's chain matches target,
+// but if so, sets target to that error value.
+func (a *Assertions) NotErrorAsf(err error, target interface{}, msg string, args ...interface{}) bool {
+	if h, ok := a.t.(tHelper); ok {
+		h.Helper()
+	}
+	return NotErrorAsf(a.t, err, target, msg, args...)
+}
+
 // NotErrorIs asserts that none of the errors in err's chain matches target.
 // This is a wrapper for errors.Is.
 func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) bool {
diff --git a/assert/assertions.go b/assert/assertions.go
index 104a0c9..44b854d 100644
--- a/assert/assertions.go
+++ b/assert/assertions.go
@@ -2149,6 +2149,24 @@ func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{
 	), msgAndArgs...)
 }
 
+// NotErrorAs asserts that none of the errors in err's chain matches target,
+// but if so, sets target to that error value.
+func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
+	if !errors.As(err, target) {
+		return true
+	}
+
+	chain := buildErrorChainString(err)
+
+	return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+
+		"found: %q\n"+
+		"in chain: %s", target, chain,
+	), msgAndArgs...)
+}
+
 func buildErrorChainString(err error) string {
 	if err == nil {
 		return ""
diff --git a/require/require.go b/require/require.go
index d0c73ff..50ec19e 100644
--- a/require/require.go
+++ b/require/require.go
@@ -1559,6 +1559,30 @@ func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string,
 	t.FailNow()
 }
 
+// NotErrorAs asserts that none of the errors in err's chain matches target,
+// but if so, sets target to that error value.
+func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
+	if assert.NotErrorAs(t, err, target, msgAndArgs...) {
+		return
+	}
+	t.FailNow()
+}
+
+// NotErrorAsf asserts that none of the errors in err's chain matches target,
+// but if so, sets target to that error value.
+func NotErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) {
+	if h, ok := t.(tHelper); ok {
+		h.Helper()
+	}
+	if assert.NotErrorAsf(t, err, target, msg, args...) {
+		return
+	}
+	t.FailNow()
+}
+
 // NotErrorIs asserts that none of the errors in err's chain matches target.
 // This is a wrapper for errors.Is.
 func NotErrorIs(t TestingT, err error, target error, msgAndArgs ...interface{}) {
diff --git a/require/require_forward.go b/require/require_forward.go
index 3c15ca3..1bd8730 100644
--- a/require/require_forward.go
+++ b/require/require_forward.go
@@ -1235,6 +1235,24 @@ func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg str
 	NotEqualf(a.t, expected, actual, msg, args...)
 }
 
+// NotErrorAs asserts that none of the errors in err's chain matches target,
+// but if so, sets target to that error value.
+func (a *Assertions) NotErrorAs(err error, target interface{}, msgAndArgs ...interface{}) {
+	if h, ok := a.t.(tHelper); ok {
+		h.Helper()
+	}
+	NotErrorAs(a.t, err, target, msgAndArgs...)
+}
+
+// NotErrorAsf asserts that none of the errors in err's chain matches target,
+// but if so, sets target to that error value.
+func (a *Assertions) NotErrorAsf(err error, target interface{}, msg string, args ...interface{}) {
+	if h, ok := a.t.(tHelper); ok {
+		h.Helper()
+	}
+	NotErrorAsf(a.t, err, target, msg, args...)
+}
+
 // NotErrorIs asserts that none of the errors in err's chain matches target.
 // This is a wrapper for errors.Is.
 func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) {


Test patch:
diff --git a/assert/assertions_test.go b/assert/assertions_test.go
index 064b92f..20d63b1 100644
--- a/assert/assertions_test.go
+++ b/assert/assertions_test.go
@@ -3269,7 +3269,6 @@ func TestNotErrorIs(t *testing.T) {
 }
 
 func TestErrorAs(t *testing.T) {
-	mockT := new(testing.T)
 	tests := []struct {
 		err    error
 		result bool
@@ -3282,9 +3281,38 @@ func TestErrorAs(t *testing.T) {
 		tt := tt
 		var target *customError
 		t.Run(fmt.Sprintf("ErrorAs(%#v,%#v)", tt.err, target), func(t *testing.T) {
+			mockT := new(testing.T)
 			res := ErrorAs(mockT, tt.err, &target)
 			if res != tt.result {
-				t.Errorf("ErrorAs(%#v,%#v) should return %t)", tt.err, target, tt.result)
+				t.Errorf("ErrorAs(%#v,%#v) should return %t", tt.err, target, tt.result)
+			}
+			if res == mockT.Failed() {
+				t.Errorf("The test result (%t) should be reflected in the testing.T type (%t)", res, !mockT.Failed())
+			}
+		})
+	}
+}
+
+func TestNotErrorAs(t *testing.T) {
+	tests := []struct {
+		err    error
+		result bool
+	}{
+		{fmt.Errorf("wrap: %w", &customError{}), false},
+		{io.EOF, true},
+		{nil, true},
+	}
+	for _, tt := range tests {
+		tt := tt
+		var target *customError
+		t.Run(fmt.Sprintf("NotErrorAs(%#v,%#v)", tt.err, target), func(t *testing.T) {
+			mockT := new(testing.T)
+			res := NotErrorAs(mockT, tt.err, &target)
+			if res != tt.result {
+				t.Errorf("NotErrorAs(%#v,%#v) should not return %t", tt.err, target, tt.result)
+			}
+			if res == mockT.Failed() {
+				t.Errorf("The test result (%t) should be reflected in the testing.T type (%t)", res, !mockT.Failed())
 			}
 		})
 	}