-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
proposal: errors: add Like for testing that error values appear as expected #49172
Comments
See https://pkg.go.dev/upspin.io/errors and https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html. As explained in the article, to work well this functionality tends to be application-specific. Your proposal puts too much weight on equality to be widely useful. Things like user names and file names must be factored out. |
Comparing error strings should be a last resort, when other methods of classification fail. And even then, it would be better to look for stable substrings in the message instead of comparing it whole, for the reason Rob mentioned above. |
It is best to only test what you promise your library users. Here you either over promise or over test. Testing for presence or absence of a specific error with some checks on information carrying errors is good enough in my book. |
@robpike I definitely appreciate your advocacy of error values and upspin.io/errors is an interesting package. It’s been my experience that very few applications and libraries have a custom errors package:
@jba The proposed errors.Like is meant to make it easier for tests to check the wrapped error, in addition to the error string:
@D1CED I agree that libraries must test what they promise to users:
|
This proposal has been added to the active column of the proposals project |
This seems to me to be an excellent argument against adding a function like this. We should not encourage treating error strings as part of a package's API. |
@neild Assuming this isn’t a snarky comment, why is it a bad thing for error strings to be part of a package’s API? For better or for worse, I have a few of arguments that I can come up with, in no particular order, along with some responses:
|
@sflaw:
Yes, it should be tested with errors.Is or errors.As. If you want to test that the error string has the text "additional context", then you would use strings.Contains, not
Almost nothing in the standard library precludes alternatives, but everything in it is supposed to embody best practices. Comparing errors strings for equality is not a best practice.
I missed that. That part of the design I agree with. |
Ad hoc error text matching is simply not a good API. There are much better alternatives.
This is not the position taken for the Go standard library. We do not consider changes to error text to be backwards-incompatible changes, and therefore can improve errors without violating the Go compatibility promise. (We have on occasion reverted error text changes when the practical impact was large, although arguably doing so sets a bad precedent.) |
@jba I think what you’re saying is that we’d be encouraging people to write brittle tests? Given this example: func TestFunc(t *testing.T) {
wantErr := fs.ErrNotExist
wantErrText := "additional context: "
err := my.Func()
if !errors.Is(err, wantErr) {
t.Fatalf("err %v is not %v", err, wantErr)
}
if err != nil && !strings.Contains(err.Error(), wantErrText) {
t.Fatalf("err %v does not contain %v", err, wantErrText)
}
} It seems like you’re arguing that I think that’s a legitimate concern that I haven’t encountered in our tests, but I definitely acknowledge that this could happen. Would you have similar objections if errorstest.Like were less strict about wrapped errors? |
@neild I’m fine with anything I haven’t quoted below:
Accidentally breaking programs when changing error strings is the motivation for this proposal! We have been burned by an innocent change to error strings because we had downstream systems relying on that text. We noticed that many of our tests had poor coverage over error values because testing errors is clumsy and unpleasant. The proposed errorstest.Like, or some better alternative, is meant to encourage assertions on error values and discourage arbitrary changes. I suppose there is this fundamental disconnect between:
|
Perhaps this proposal is solving the wrong problem? Perhaps the goal should be to encourage an idiomatic way of using errors? If that’s the case, I would be happy to submit an alternate proposal where we do one or more of the following:
Obviously, some of these recommendations are outlandish, but I’ve included them for completeness. Each of these changes would shift the burden of error string compatibility from upstream library authors to downstream library consumers. Are people more sympathetic to this potential future? |
This seems reasonable to me.
Checks in
If we were designing the standard library from scratch today, I would argue for introducing randomness in error text when running tests. Alas, it is much too late now; that change would be a clear violation of the Go compatibility guarantee. It might be feasible to have a build tag that randomizes the text of every error for testing, but that would rely on users knowing about and testing with it set. Nothing stops you from doing something in your own packages, of course. For example, the |
By our definition of error strings, are we sure that comparing error.Error() to a string isn’t actually a subtle bug? If Go itself can change the error strings between minor releases, then this is a potential landmine for any developer. For an even weaker suggestion, perhaps we add stringcompareerrors to x/tools/go/analysis/passes, which defines an Analyzer that checks for the use of comparisons with error strings? There’s a similar package, deepequalerrors that isn’t shipped with go vet, but is used by metalinters.
I’m being a little contrarian here, but here’s a thought experiment: If we “do not consider changes to error text to be backwards-incompatible changes, and therefore can improve errors without violating the Go compatibility promise”, then surely it follows that we could break error string comparisons as long as the output of errors.New and fmt.Errorf look the same? Build flags like -race or -shuffle are being added to CI, so it seems reasonable to suggest an -errfuzz flag? If we used a build flag, we could change errors.New to read: // New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{fuzz(text)}
}
const wordJoiner = "\u2060"
var fuzzer = strings.NewReplacer("", wordJoiner) // TODO: add randomness
func fuzz(text string) string {
if runtime.errfuzzenabled {
return fuzzer.Replace(text)
}
return text
} |
Seems reasonable to me, although I'm not the decider for that package.
We could technically break string comparisons against errors generated by the standard library. However, Additionally, while changing the text of every error in the stdlib is technically within the bounds of the compatibility promise, I think it would be difficult to justify the cost. |
If this change only applied to go test, while protected behind an -errfuzz flag, is the cost really that high? It seems unlikely that anyone would want to build this into their actual binaries, so I would explicitly avoid having this flag for build or run commands. The advantage to fuzzing error strings, instead of writing some kind of analyzer, is that there would be no false positives. As an aside, I tried to think of a way to add error string fuzzing outside the standard library, but I don’t think that would work reliably. |
Based on the discussion above, this proposal seems like a likely decline. |
@rsc Agreed. It looks like we will take another path. |
No change in consensus, so declined. |
Problem
Currently, there is no general way to test whether a returned error value is what the developer expected:
Ideally, a developer could write the following test:
Workarounds
Often, we see a helper function inside the test suite that helps compare Error text by dealing with nil errors:
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
Proposed implementation
The following is a proposed implementation of errors.Like:
You can also find an implementation with test cases in the Go Playground: https://play.golang.org/p/qnBbkSbMlLO
See also
The text was updated successfully, but these errors were encountered: