初衷

今天要聊的库就是 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。

用法

我自己用了一段时间后感觉用起来挺不错了,感觉可以推荐更多人来使用并欢迎大家提出各种建议。这个库可以作为任何测试框架和库的补充来使用,让测试代码的可读性更高。

包assert为开发人员提供了一种在案例失败时自动断言表达式并输出有用的上下文信息的方法。有了这个包,我们可以专注于编写测试代码,而不必担心如何打印大量冗长的调试信息进行调试。

这是一个快速示例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import "github.com/huandu/go-assert"

func TestSomething(t *testing.T) {
    str := Foo(42)
    assert.Assert(t, str == "expected")

    // This case fails with following message.
    //
    //     Assertion failed:
    //         str == "expected"
    //     Referenced variables are assigned in following statements:
    //         str := Foo(42)
}

使用go get安装该软件包。

1
go get  github.com/huandu/go-assert

当前的稳定版本是v1.*. 标记为 的旧版本v0.*已过时。

断言方法

如果我们只想使用Assert,Equal或NotEqual之类的函数,建议将此包导入为..

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import "github.com/huandu/go-assert"

func TestSomething(t *testing.T) {
    a, b := 1, 2
    assert.Assert(t, a > b)

    // This case fails with message:
    //     Assertion failed:
    //         a > b
}

func TestAssertEquality(t *testing.T) {
    assert.Equal(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]
}

高级断言包装器:类型 A

如果我们想在断言更多的控制,建议包裹t在A。

在 A 中实现了许多有用的断言方法。

  • Assert/ Eqaul/ NotEqual:基本断言方法。
  • NilError/ NonNilError: 测试函数/方法是否返回预期错误。
  • Use:跟踪变量。如果任何断言方法失败,则断言方法中跟踪A和关联的所有变量都将在断言消息中自动打印出来。

这是一个示例,演示如何使用A#Use在断言消息中打印相关变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "github.com/huandu/go-assert"

func TestSomething(t *testing.T) {
    a := assert.New(t)
    v1 := 123
    v2 := []string{"wrong", "right"}
    v3 := v2[0]
    v4 := "not related"
    a.Use(&v1, &v2, &v3, &v4)

    a.Assert(v1 == 123 && v3 == "right")

    // This case fails with following message.
    //
    //     Assertion failed:
    //         v1 == 123 && v3 == "right"
    //     Referenced variables are assigned in following statements:
    //         v1 := 123
    //         v3 := v2[0]
    //     Related variables:
    //         v1 -> (int)123
    //         v2 -> ([]string)[wrong right]
    //         v3 -> (string)wrong
}

转载

go-assert 库介绍