Reputation: 1415
I got a problem like below: Compare 2 errors when writing unit test
package main
import (
"errors"
"fmt"
"reflect"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func main() {
er1 := errors.New("database name is not exists")
er2 := errors.New("database name is not exists")
result1 := reflect.DeepEqual(er1, er2)
fmt.Println("reflect: ", result1)
result2 := cmp.Equal(er1, er2, cmpopts.EquateErrors())
fmt.Println("compare: ", result2)
result3 := errors.Is(er1, er2)
fmt.Println("errorIs: ", result3)
}
And the output of the above code is:
reflect: true
compare: false
errorIs: false
I want to compare 2 error and reflect.DeepEqual(er1, er2)
is the 1st approach I apply and this method produce the output I want, but this approach have a warning from go lint
:
avoid using reflect.DeepEqual with errorsdeepequalerrors
After google search, some people tell me some approaches that are:
cmp.Equal(er1, er2, cmpopts.EquateErrors())
errors.Is(er1, er2)
But both above approaches can not produce same result like the 1st approach (use reflect.DeepEqual).
How I can compare 2 errors without warning from go lint
and produce the result like the reflect.DeepEqual
Tks
Upvotes: 9
Views: 6489
Reputation: 75
I am not sure I agree with the argument that reflect.DeepEqual should be avoided for errors. I have been in both situations where I was not able to use reflect.DeepEqual and, more commonly, where I needed to be sure of the error type and already knew the error message (i.e. you can log the full database connection error for internal debugging. Then you replace the error with a custom InternalServerErr and a message with non-sensitive information to the user, like "Could not access internal resources" as the user does not need to know anything about the details of your internal infrastructure, datastores, etc.. which could be revealed by a database error. In this scenario we both have a specific type of error and specific lexical content: a perfect use case for reflect.DeepEqual
If your error has dynamic lexical content (as would be returned say from a failed database connection attempt) you could add a known string to the error message and use strings.Contains as others have stated. But, if you know the error type and lexical value you are looking for and those are important to your business logic then reflect.DeepEqual is completely valid and very useful for such comparisons.
In the example below I am outputting the error type as well as the value. This is because I need to know that the generic error returned by my data access layer was turned into a custom InternalServiceErr.
if !reflect.DeepEqual(test.expect.err, err) {
t.Errorf(
"\tFAIL -> expected: %T:%v, actual: %T:%v",
test.expect.err, test.expect.err, err, err,
)
} else {
t.Log("\tSuccess")
}
Then I can take advantage of code-reuse by creating a function to return errors at the handler level, with a relevant HTTP Status. DeepEqual works very well for testing this functionality, again provided you can predict the lexical content of your error. Example below:
func WriteErrorResponse(w http.ResponseWriter, err error) {
var statusCode int
switch err.(type) {
case *types.BadRequestErr:
statusCode = http.StatusBadRequest
case *types.InternalServerErr:
statusCode = http.StatusInternalServerError
case *types.MethodNotAllowedErr:
statusCode = http.StatusMethodNotAllowed
case *types.UnAuthorized:
statusCode = http.StatusForbidden
case *types.UnAuthenticated:
statusCode = http.StatusUnauthorized
case *types.NotFoundErr:
statusCode = http.StatusNotFound
default:
statusCode = http.StatusInternalServerError
}
WriteResponse(w, err, statusCode)
}
Much of this depends on the application you are creating, but reflect.DeepEqual should not be highlighted as an anti-pattern within VS Code, there are definitely legitimate use cases for comparing errors with reflect.DeepEqual
Upvotes: 0
Reputation: 51840
Depending on how you write your tests, you may depend on reflect.DeepEqual()
and ignore the linter warning ;
the drawback is : you start depending on the inner structure of the errors you return.
In the testing code we write, we use one of the following patterns :
nil
;package pkg
var ErrUnboltedGizmo = errors.New("gizmo is unbolted")
// in test functions, depending on the case :
if err == pkg.ErrUnboltedGizmo { ...
// or :
if errors.Is(err, pkg.ErrUnboltedGizmo) { ...
io.EOF
), we write code that dutifully wraps that known error, and we use errors.Is()
(both in production code and in testing code),Parse error
and not File not found
), we simply search for strings in the error message :if err == nil || !strings.Contains(err.Error(), "database name is not exists") {
t.Errorf("unexpected error : %s", err)
}
Upvotes: 4
Reputation: 386
What I found useful is to use cmp.Diff with cmpopts.IgnoreFields to ignore the sepecific error fields which cause the issue u detailed and afterward I just run a check on the errors with string.Contains or whatever I find fitting.
So it goes something like this:
if diff := cmp.Diff(expected, got, cmpopts.IgnoreFields(expected, "ErrorField")); diff != "" {
// found diff not including the error
}
Now check only the errors on their own and that's it.
And yeah I know u marked a solution already but maybe it will help someone :)
Upvotes: 1