Compare 2 errors in Go for unit test - go

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:
Use cmp package to compare: cmp.Equal(er1, er2, cmpopts.EquateErrors())
Use errors package to compare: 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

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 I read, and the testing code we write, we use one of the following patterns :
most of the time, we just compare the error to nil ;
in some cases our functions return predefined error values, and we test for these specific values :
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) { ...
when our production code mandates that a specific error be spotted (a common use case is io.EOF), we write code that dutifully wraps that known error, and we use errors.Is() (both in production code and in testing code),
when the need is, in testing only, to loosely confirm that an error matches something and not something else (e.g : 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)
}

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 :)

Related

Go - Handle Errors Conditionally

I have a code where I clone multiple gitlab repositories. The library I am depending on is "gopkg.in/src-d/go-git.v4". The clone function will return an error if a repository already exists.
I want to ignore this error and continue the loop of clonning repoistories. Below is my attempt to solve the issue by using errors.New() However, it does not work since the returned err and the new error do not match.
import (
gitgo "gopkg.in/src-d/go-git.v4"
"log"
"errors"
)
var errRepoIsThere = errors.New("repository already exists")
_, err := gitgo.PlainClone(repoLocalPath, false, &gitgo.CloneOptions{})
if !errors.Is(err, errRepoIsThere) {
log.Fatal(err)
}
the error returned from gitgo.PlainClone is as defined here:
https://pkg.go.dev/github.com/go-git/go-git/v5#pkg-variables
ErrRepositoryNotExists = errors.New("repository does not exist")
I've went through this question
How to compare Go errors and saw that all awnsers discourage the use of err.Error() == err2.Error() type of error handling.
What would be the right approach for my issue in this case?
That error is a package level var - essentially, a singleton - so comparison is appropriate:
err == gitgo.ErrRepositoryNotExists
Comparing .Error() is considered poor practice because error text is incidental (but package exports are assumed reliable)

Why doesn't t.Fail() accept string arguments?

I am trying to improve my Golang tests. And I was reading this: https://ieftimov.com/post/testing-in-go-failing-tests/
I was using t.Fatal("message") a lot, when instead I should have been using a combination of:
t.Fail()
t.Logf()
so why on Earth is there not a single call then can fail the test and log the reason why? Is there a way for me to add such a method to a test.Testing instance? I just want to do:
t.FailWithReason("the reason the test failed")
does this exist and if not can I add it somehow?
Take a look at the documentation and source code for the testing package.
The documentation has an example of typical use:
func TestAbs(t *testing.T) {
got := Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %d; want 1", got)
}
}
The documentation for t.Errorf is:
// Errorf is equivalent to Logf followed by Fail.
which is very similar to what you say you want:
t.Fail()
t.Logf()

How to properly check type of error returned by plugin.Open

I would like to know how can I check the type of error returned by plugin.Open, e.g:
package main
import "plugin"
func main() {
_, err := plugin.Open("./module.so")
// here
}
I would like to do something different if the error is:
plugin.Open("./module.so"): realpath failed
Which basically means that the file doesn't exist.
Example of desired result:
package main
import "plugin"
func main() {
_, err := plugin.Open("./module.so")
if err.Error() == "plugin.Open(\"./module.so\"): realpath failed" {
// do something different here
} else {
log.Fatal(err)
}
}
The string that I pass to plugin.Open can have other values, so it needs to be something more smart than that.
Thanks in advance.
Inspection of the code for plugin.Open() reveals the package calls out to some C code to determine whether the path exists. If it doesn't, it returns a plain error value. In particular, the package does not define any sentinel errors which you can compare against, nor does it return its own concrete implementer of the error interface which carries custom metadata. This is the code which produces that error:
return nil, errors.New(`plugin.Open("` + name + `"): realpath failed`)
errors.New is a basic implementation of the error interface which doesn't allow any additional information to be passed. Unlike other locations in the standard library which return errors (such as path non-existent errors from the os package), you can't get such metadata in this instance.
Check whether the module file exists first
My preference would be to verify whether the module exists before attempting to load it, using the native capabilities provided by the os package:
modulePath := "./module.so"
if _, err := os.Stat(modulePath); os.IsNotExist(err) {
// Do whatever is required on module not existing
}
// Continue to load the module – can be another branch of the if block
// above if necessary, depending on your desired control flow.
Compare a subset of the error values
You could also use strings.Contains to search for the value realpath failed in the returned error value. This is not a good idea in the event that string changes in future, so if you adopt this pattern, at the very least you should ensure you have rigorous tests around it (and even then it's still not great).
_, err := plugin.Open("./module.so")
if err != nil {
if strings.Contains(err.Error(), "realpath failed") {
// Do your fallback behavior for module not existing
log.Fatalf("module doesn't exist")
} else {
// Some other type of error
log.Fatalf("%+v", err)
}
}

Golang not able to test in same package

Having issue creating unit test of one of my source file ( commonutil.go )
package util
import "github.com/nu7hatch/gouuid"
// GenerateUUID Returns generated UUID sequence
func GenerateUniqueID(hostname string) (string, error) {
var result, err = uuid.NewV4()
return hostname + ":" + result.String(), err
}
For the above source, I created the test file "commonutil_test.go" ( in the same package )
package util
import "testing"
func TestCommonUtil(t *testing.T) {
t.Run("Test generate UUID", func(t *testing.T) {
var uuid, _ = GenerateUniqueID ("test")
//fmt.Printf("UUID isa %v \n", uuid)
if uuid == "" {
t.Errorf("UUID expected, but result is empty ")
}
})
However when trying executing "go test util/commonutil_test.go" it shows :
util\commonutil_test.go:8: undefined: GenerateUniqueID
FAIL command-line-arguments [build failed]
Changing to util.GenerateUniqueID in the test solve the problem, however when running coverage using Goconvey will cause build failure :
can't load package: import cycle not allowed
package rudygunawan.com/MyProject/HFLC-Go/util
imports rudygunawan.com/MyProject/HFLC-Go/util
Any idea to solve this issue? I am confused.
Go version is go1.7.1 windows/386
I've run into a similar problem, when I was trying to run a single test file.
I wanted that, as it was a kind of test driven development thing, where I wanted to run tests only for the code I was working on at the moment, and not all the x-minutes running tests.
The solution turned out to be not running tests from a file, but rather running a specific test by name (actually a regex). So in your case I guess it would be:
go test ./util -run TestCommonUtil
An alternative seems to be listing all the files needed to build your test code:
go test util/commonutil_test.go util/commonutil.go
Just realize it is a silly mistake. The package for the test should be "util_test". Putting the test in the separate package ( but still in the same folder) help solve import cycle issue, yet still allow to solve the undefined error.
The way I normally write Go unit tests is to have one (or more) ..._test.go files, in the same package as the code being tested, with one Test... function for each broad set of tests to be done.
package util
import "testing
func TestGenerateUniqueID(t *testing.T) {
var uuid1, uuid2 string
uuid1, err = GenerateUniqueID("test")
if err != nil {
t.Errorf("Expected no error, got %s", err) // Maybe Fatalf?
}
if uuid1 == "" {
t.Errorf("Expected non-empty string, got empty string (uuid1)")
}
uuid2, err = GenerateUniqueID("test")
if err != nil {
t.Errorf("Expected no error, got %s", err) // Maybe Fatalf?
}
if uuid2 == "" {
t.Errorf("Expected non-empty string, got empty string (uuid2)")
}
if uuid1 == uuid2 {
t.Errorf("Expected uuid1 and uuid2 to be different, both are %s", uuid1)
}
}
One of the reasons I tend towards whitebox testing (where I can do "blackbox testing" by carefully not accessing package internals) is that there's usually a whole slew of non-exported code that really should be tested as well. In this specific, small, example, there's no massive argument for one over the other, since all the functionality that can be tested is already exported.

Idiomatically filtering errors in Go

The error type in Go is quite broad, and the actual type contained can vary by platform as well as between versions. Is it idiomatic to filter errors that we expect by the error's string value, error.Error()?
For example:
_, err := conn.Write(b)
if err != nil {
if !strings.Contains(err.Error(), "peer reset") {
log.Print(err)
}
return
}
Is there a better way to do this?
I'm not sure I'm saying anything you don't know, but I've seen a few ways to handle it: compare to known instances when they are exported and appear in the godoc, like io.EOF or os.ErrNotExist; using type assertions or switches when the docs promise an error of a certain type, like *os.PathError or *os.LinkError; or giving up and looking at messages. The first should be clear enough and you've done the third in your question; checking for type could look like:
if pathErr, ok := err.(*os.PathError); ok {
log.Fatal("Bad path " + pathErr.Path + ", giving up")
} else if err != nil {
return err
}
I think this would be rare, but if you had several potential types involved, you could conceivably use a type switch:
switch err.(type) {
case *os.PathError, *os.LinkError:
log.Fatal("oof")
case nil:
// nothing; prevents default
default:
log.Fatal("huh")
}
(The choice of error and behavior here is a bit silly--I'm just trying to put up a template with the syntax.)
The hard part you allude to in your question: it may not be clear what guarantees you have about what errors will be returned. I know that, for example, the 1.5 release notes say that they're standardizing more on net.OpError for net errors than before, but that's all I know. Spelunking in the source of the package you're calling, or asking people questions, may be in order if there are important cases you think are unclear.
errors.Is and errors.As added in more recent versions of Go would now address this problem.

Resources