go-assert 库介绍
断言失败时只看到一行 Fatalf 太难受了,go-assert 用 runtime.Callers 和 go/parser 把出错的表达式原文打印出来。
最后更新于
- #go
- #testing
- #go-assert
闲来写写码。其实最近几天不闲,但是因为疫情的关系在家办公,安静写代码之余也将工作中用到的自己写的开源库做了些加强,方便自己也方便他人。
初衷
今天要聊的库就是 github.com/huandu/go-assert,是我在几年前突发奇想实现的库。当时有一个「痒点」:写 Go 测试用例的时候希望能将上下文自动输出到日志里,方便在 case 失败的时候做调试。以前在 C 时代我们都使用 assert 或者类似宏来输出更多一些信息,比如 assert(a > b) 时候输出 a > b 这行代码。很显然,在 Go 里面用 t.Fatalf 肯定做不到这种效果。要实现也不难,考虑到一般情况下我们都是使用 go test 命令直接测试,而不会生成二进制并且发布到另外的机器上执行,所以这意味着运行测试用例时候我们基本都能同时访问到源码文件本身,那么我们需要做的事情就是在测试失败的时候读取对应位置源码并且显示出来就好了。
基本思路
最初的版本实现的非常简单:实现一个函数 Assert(t *testing.T, expr interface{}),如果 expr的值为「非真值」,包括 nil、false、各种数值量零值等,则调用 t.Fatalf输出错误。 在输出错误时,通过 runtime.Callers 找到调用函数的文件名、行号和函数名,有了这个之后,假设这个文件可以通过 os.Open 读取到,那么就交给 Go 的语法解析器 go/parser 来解析。一般来说,既然可以执行测试用例,源文件肯定不会有什么语法错误,顺利得到 AST 之后就可以通过 ast.Inspect 找到当前调用的函数,拿到函数的参数代码,于是就能顺利的打印出类似 assert(a > b) 的 a > b 部分代码啦。
更友好的变量信息输出
后来自己在使用过程中发现仅仅打印源码中的表达式并不够好,更多时候我们是在比较两个值是否相等,不等的时候需要通过错误日志查看失败原因,因此就又实现了一个 AssertEqual(t *testing.T, v1, v2 interface{})。这里面有两个特殊功能点可以稍微说明一下。
首先是在测试用例失败时如何很好的输出 v1 和 v2 的值,这里用到了 github.com/davecgh/go-spew,这是一个很好的打印变量值的库,它可以以一种非常可读且稳定的方法将任意 Go 变量输出到日志里面去,比 fmt 自带的 %v 或 %#v 的输出看起来好多了。
其次是怎么能尽可能提供更多测试上下文,比如测试用例中写 AssertEqual(t, a, b),如果调试信息里面能够输出 a 和 b 的最后一个赋值语句就好了,这样我一眼就能看出这个测试用例哪里出错了。要实现这个功能也不难,结合前面所说打印源码的原理,这次无非就是往 AssertEqual 函数调用前找到最后一个跟 a 或 b 相关的赋值语句即可,这里就不赘述了,代码实现详见 assertion.go:310 func ParseArgs。
用法
我自己用了一段时间后感觉用起来挺不错了,感觉可以推荐更多人来使用并欢迎大家提出各种建议。这个库可以作为任何测试框架和库的补充来使用,让测试代码的可读性更高。
下面内容直接复制自 v1.0.2 的 README。
Package assert provides developer a way to assert expression and print expression source code in test cases. It works like C macro assert. When assertion fails, source code of the expression in assert function is printed.
For example, if we write Assert(t, a > b) when a = 1 and b = 2, we can read Assertion failed: a > b in the failure message. The a > b is the expression evaluated in Assert.
Without this package, developers must use negate logic to test expressions and call t.Fatalf with lots of redundant information to print meaningful failure message for debugging. Just like following.
func TestSomething(t *testing.T) {
str := Foo(42)
// We expect `str` to be "expected". To verify it, we need to use negate logic.
// It's not straight forward.
if str != "expected" {
// We have to write some messages to let us know what's called and why it fails.
t.Fatalf("invalid str when calling Foo(42). [str:%v] [expected:%v]", str, "expected")
}
}
With this package, we can significantly simplify test code which works similar as above.
import . "github.com/huandu/go-assert"
func TestSomething(t *testing.T) {
str := Foo(42)
Assert(t, str == "expected")
// This case fails with following message.
//
// Assertion failed:
// str == "expected"
// If we're aware of the value of str, use AssertEqual.
AssertEqual(t, str, "expected")
// This case fails with following message with lots of useful information.
//
// Assertion failed:
// The value of following expression should equal.
// [1] str
// str := Foo(42)
// [2] "expected"
// Values:
// [1] = (string)actual
// [2] = (string)expected
}
Import
Use go get to install this package.
go get github.com/huandu/go-assert
Current stable version is v1.*. Old versions tagged by v0.* are obsoleted.
Usage
If we just want to use functions like Assert or AssertEqual, it’s recommended to import this package as ..
import . "github.com/huandu/go-assert"
func TestSomething(t *testing.T) {
a, b := 1, 2
Assert(t, a > b)
// This case fails with message:
// Assertion failed:
// a > b
}
func TestAssertEquality(t *testing.T) {
AssertEqual(t, map[string]int{
"foo": 1,
"bar": -2,
}, map[string]int{
"bar": -2,
"foo": 10000,
})
// This case fails with message:
// Assertion failed:
// The value of following expression should equal.
// [1] map[string]int{
// "foo": 1,
// "bar": -2,
// }
// [2] map[string]int{
// "bar": -2,
// "foo": 10000,
// }
// Values:
// [1] = (map[string]int)map[bar:-2 foo:1]
// [2] = (map[string]int)map[bar:-2 foo:10000]
}
If we want more controls on assertion, it’s recommended to wrap t in an A. One huge benifit of using A is that it can assert a function call directly like following.
import "github.com/huandu/go-assert"
func TestCallAFunction(t *testing.T) {
a := assert.New(t)
f := func(bool, int) (int, string, error) {
return 0, "", errors.New("an error")
}
a.NilError(f(true, 42)) // Assert calls to f to make test code more readable.
// This case fails with message:
// Assertion failed:
// Following expression should return a nil error.
// f(true, 42)
// The error is:
// an error
}
本文最初于 2020-02-12 发布在知乎。