20

我第一次尝试对 Go 包进行单元测试,但在同一个文件中有几个错误。

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"
}

但是,所有命名约定似乎都是这样的func TestXxx(*testing.T)来自testing包文档)。这意味着我的测试文件将如下所示:

func TestError (t *testing.T) { ... } // FooErr
func TestError (t *testing.T) { ... } // BarErr

这显然是同一个签名的两个函数。处理此问题的推荐方法是什么?

4

4 回答 4

38

这里有几点需要考虑:

错误

包级导出的错误值通常命名Err后跟一些东西,例如ErrTimeout here。这样做是为了让您的包裹的客户可以执行类似的操作

if err := yourpkg.Function(); err == yourpkg.ErrTimeout {
  // timeout
} else if err != nil {
  // some other error
}

为了促进这一点,它们通常使用以下方式创建errors.New

// Error constants
var (
  ErrTimeout = errors.New("yourpkg: connect timeout")
  ErrInvalid = errors.New("yourpkg: invalid configuration")
)

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

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] }

后一种方法的一个优点是它不能与任何其他包中的类型进行比较。

如果您在错误中需要一些额外数据,则类型的名称以 结尾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 {
  // syntax error
} else if err != nil {
  // other error
}

在任何一种情况下,记录您的代码都很重要,以便您的包的用户了解何时使用它们以及哪些函数可能会返回它们。

测试

测试通常以他们正在测试的单元命名。在许多情况下,您不会单独测试错误条件,因此TestError这个名称不应该经常出现。然而,测试本身的名称必须是唯一的,并且不限于以与示例相同的方式匹配被测代码中的任何内容。当您测试一段代码的多个条件时,通常最好将测试制定为Table Driven Test。该 wiki 页面有一些很好的示例,但为了演示错误检查,您可以这样做:

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
  }
}

如果你确实需要一个特殊的测试函数来为一个单元做一些奇怪的事情并且不适合主测试,你通常会把它命名为一个描述性的东西TestParseTimeout,包括你正在测试的单元和行为。

于 2013-03-02T02:39:25.090 回答
9

我将遵循测试包概述部分中记录的示例功能的约定:

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

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

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

于 2013-03-01T05:49:15.860 回答
5

您不需要让 Xxx 部分TestXxx匹配实际的函数名称。为测试添加前缀的约定Test足以让go test命令获取它们。

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

于 2013-03-01T00:50:26.123 回答
2

Go 1.4(2014 年第四季度)将为测试方法增加一种命名约定:

测试包有一个新的工具来提供对运行一组测试的更多控制。
如果测试代码包含一个函数:

func TestMain(m *testing.M) 

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

于 2014-10-30T08:36:27.667 回答