I'm trying to unit test a Go package for the first time, and I have a couple of errors in the same file.
type FooErr int
type BarErr int
func (e *FooErr) Error () string {
return "A Foo Error has occurred"
}
func (e *BarErr) Error () string {
return "A Bar Error has occurred"
}
However, all naming conventions seem to look like this func TestXxx(*testing.T)
(from the testing
package documentation). This would mean my testing file would look like this:
func TestError (t *testing.T) { ... } // FooErr
func TestError (t *testing.T) { ... } // BarErr
Which is obviously two functions of the same signature. What is the recommended method for handling this?
There are a few things to consider here:
Package-level exported error values are typically named Err
followed by something, for instance ErrTimeout
here. This is done so that clients of your package can do something like
if err := yourpkg.Function(); err == yourpkg.ErrTimeout {
// timeout
} else if err != nil {
// some other error
}
To facilitate this, they are often created either with errors.New
:
// Error constants
var (
ErrTimeout = errors.New("yourpkg: connect timeout")
ErrInvalid = errors.New("yourpkg: invalid configuration")
)
or with a custom, unexported type:
type yourpkgError int
// Error constants
var (
ErrTimeout yourpkgError = iota
ErrSyntax
ErrConfig
ErrInvalid
)
var errText = map[yourpkgError]string{
ErrTimeout: "yourpkg: connect timed out",
...
}
func (e yourpkgError) Error() string { return errText[e] }
One advantage of the latter approach is that it cannot compare equal with a type from any other package.
In the case where you need some extra data inside the error, the name of the type ends in Error
:
type SyntaxError struct {
File string
Line, Position int
Description string
}
func (e *SyntaxError) Error() string {
return fmt.Sprintf("%s:%d:%d: %s", e.File, e.Line, e.Position, e.Description)
}
which, in contrast to the previous equality check, requires a type assertion:
tree, err := yourpkg.Parse(file)
if serr, ok := err.(*SyntaxError); ok {
// syntax error
} else if err != nil {
// other error
}
In either case, it is important to document your code so that users of your package understand when they will be used and what functions might return them.
Tests are often named after the unit that they're testing. In many cases, you won't test error conditions separately, so TestError
is not a name that should come up very often. The name of the test itself merely has to be unique, however, and is not constrained to match anything in the code under test in the same way that examples are. When you're testing multiple conditions of a piece of code, it is often best to formulate the test as a Table Driven Test. That wiki page has some good examples, but to demonstrate error checking, you might do this:
func TestParse(t *testing.T) {
tests := []struct{
contents string
err error
}{
{"1st", nil},
{"2nd", nil},
{"third", nil},
{"blah", ErrBadOrdinal},
{"", ErrUnexpectedEOF},
}
for _, test := range tests {
file := strings.NewReader(test.contents)
if err := Parse(file); err != test.err {
t.Errorf("Parse(%q) error %q, want error %q", test.contents, err, test.err)
}
// other stuff
}
}
If you do need a special test function for a unit that does something weird and doesn't fit in the main test, you'd typically name it something descriptive like TestParseTimeout
that includes both the unit and the behavior you're testing.
I would follow the convention for example functions documented in the overview section of the testing package:
"The naming convention to declare examples for a function F, a type T and method M on type T are:"
func ExampleF() { ... }
func ExampleT() { ... }
func ExampleT_M() { ... }
The naming convention for example functions is needed for godoc, but I'd follow the same convention for tests, TestT_M, for consistency.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With