A common practice in Go unit testing is to check whether an expected error is not nil. Errors are just as important to the API contract as returned data and function definitions. Changes to error values can lead to backward compatibility issues and unexpected behavior in user applications. By verifying that the correct errors are returned, unit tests can help detect unintended changes or mistakes in both your code and its dependencies.
Common Unit Test
IntelliJ Pattern
A simple function to upper case a string. Returns an ErrorEmptyString if s
is empty
var ErrorEmptyString = errors.New("empty string")
func toUpperCase(s string) (string, error) {
if s == "" {
return "", fmt.Errorf("Passend in empty string: %w", ErrorEmptyString)
}
return strings.ToUpper(s), nil
}
IntelliJ/GoLand generated test only testing if an error occurs.
func Test_toUpperCase(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{args: args{s: ""}, want: "", wantErr: true},
{args: args{s: "asdf"}, want: "ASDF", wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toUpperCase(tt.args.s)
if (err != nil) != tt.wantErr {
t.Errorf("toUpperCase() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("toUpperCase() got = %v, want %v", got, tt.want)
}
})
}
}
Testing Out Fixes
Working with err != ErrorEmptyString
At first glance it would seem easy enough to just change wantErr bool
to wantErr error
then modify the error detection to compare wantErr to EmptyStringError.
tests := []struct {
...
wantErr error
}{
{args: args{s: ""}, want: "", wantErr: ErrorEmptyString},
}
if err != nil && err != tt.wantErr {
t.Errorf("toUpperCase() error = %v, wantErr %v", err, tt.wantErr)
...
This change seems ok at first glance, but it isn’t exactly great. If the error returned is wrapped with fmt.Errorf("the error: %w",ErrorEmptyString")
. Errors in go are just strings and a wrapped error may not match the string in ErrorEmptyString
. When this test is run for example it fails:
=== RUN Test_toUpperCase/#00
main_test.go:22: toUpperCase() error = Passed in empty string: empty string, wantErr empty string
Working with errors.Is()
A fix for the previous example is to replace if err != nil && err != tt.wantErr
with if err != nil && !errors.Is(err,tt.wantErr)
if err != nil && !errors.Is(err, tt.wantErr) {
t.Errorf("toUpperCase() error = %v, wantErr %v", err, tt.wantErr)
return
}
errors.Is() will automatically unwrap the error and return true
if err contains a wrapped error of ErrorEmptyString
.
Conclusion
If you want to go to the extra mile to be sure you protect yourself from unexpected changes in errors be sure you check for more than err != nil
. There are many testing packages that provide assertions and test comparisons so just be sure that they unwrap errors correctly to save yourself some headaches.