在Golang中的测试命名约定

huangapple go评论98阅读模式
英文:

Test Naming Conventions in Golang

问题

type FooErr int
type BarErr int

func (e *FooErr) Error() string {
return "发生了一个 Foo 错误"
}

func (e *BarErr) Error() string {
return "发生了一个 Bar 错误"
}

func TestErrorFoo(t *testing.T) { ... }
func TestErrorBar(t *testing.T) { ... }

英文:

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?

答案1

得分: 41

这里有几个需要考虑的事情:

错误

通常,包级别的导出错误值的命名方式是以Err开头,后面跟着一些内容,例如ErrTimeout 这里。这样做是为了让你的包的客户端可以这样做:

if err := yourpkg.Function(); err == yourpkg.ErrTimeout {
  // 超时
} else if err != nil {
  // 其他错误
}

为了方便这样做,它们通常使用errors.New创建:

// 错误常量
var (
  ErrTimeout = errors.New("yourpkg: 连接超时")
  ErrInvalid = errors.New("yourpkg: 配置无效")
)

或者使用自定义的、未导出的类型:

type yourpkgError int

// 错误常量
var (
  ErrTimeout yourpkgError = iota
  ErrSyntax
  ErrConfig
  ErrInvalid
)

var errText = map[yourpkgError]string{
  ErrTimeout: "yourpkg: 连接超时",
  ...
}

func (e yourpkgError) Error() string { return errText[e] }

后一种方法的一个优点是它不能与任何其他包的类型相等。

在需要在错误中包含一些额外数据的情况下,类型的名称以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)
}

与之前的相等性检查相比,这需要进行类型断言:

tree, err := yourpkg.Parse(file)
if serr, ok := err.(*SyntaxError); ok {
  // 语法错误
} else if err != nil {
  // 其他错误
}

无论哪种情况,重要的是要记录你的代码,以便你的包的用户了解它们何时会被使用以及哪些函数可能会返回它们。

测试

测试通常以它们测试的单元的名称命名。在许多情况下,你不会单独测试错误条件,所以TestError不是一个经常出现的名称。测试本身的名称只需要是唯一的,不需要与被测试的代码中的任何内容匹配,就像示例一样。当你测试一段代码的多个条件时,通常最好将测试构建为表驱动测试。该维基页面有一些很好的例子,但为了演示错误检查,你可以这样做:

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)
    }
    // 其他内容
  }
}

如果你确实需要一个特殊的测试函数来测试一个做一些奇怪的事情并且不适合主要测试的单元,你通常会给它起一个描述性的名称,比如TestParseTimeout,其中包含了单元和你要测试的行为。

英文:

There are a few things to consider here:

Errors

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

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.

1: http://golang.org/src/pkg/testing/iotest/reader.go?h=ErrTimeout#L69 "ErrTimeout"
2: http://code.google.com/p/go-wiki/wiki/TableDrivenTests "TableDrivenTest wiki"

答案2

得分: 10

我会遵循测试包概述部分中记录的示例函数的约定:

“声明函数F、类型T和类型T上的方法M的示例的命名约定为:”

func ExampleF() { ... }
func ExampleT() { ... }
func ExampleT_M() { ... }

示例函数的命名约定是为了godoc,但为了保持一致性,我会遵循相同的约定来命名测试函数,即TestT_M。

英文:

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.

答案3

得分: 7

你不需要让TestXxx的Xxx部分与实际的函数名匹配。只要在测试前面加上Test前缀,go test命令就能找到它们。

就像Alex Lockwood在他的评论中所说,如果你愿意,你可以使用TestFooError和TestBarError。

英文:

You don't need to have the Xxx part of TestXxx match an actual function name. The convention of prefixing the tests with Test is enough for the go test command to pick them up.

Like Alex Lockwood says in his comment, you could use TestFooError and TestBarError if you want.

答案4

得分: 2

Go 1.4 (Q4 2014) 将为测试方法添加一种新的命名约定:

> testing 包有一个新的功能,可以更好地控制一组测试的运行。
如果测试代码包含一个函数:

func TestMain(m *testing.M) 

> 那么该函数将被调用,而不是直接运行测试。
M 结构体 包含了访问和运行测试的方法。

英文:

Go 1.4 (Q4 2014) will add one more naming convention for test methods:

> The testing package has a new facility to provide more control over running a set of tests.
If the test code contains a function:

func TestMain(m *testing.M) 

> that function will be called instead of running the tests directly.
The M struct contains methods to access and run the tests.

huangapple
  • 本文由 发表于 2013年3月1日 08:29:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/15148331.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定