Go猜想录
大道至简,悟者天成
Go Test

写test的好处

  1. 寻找不易发现的bug、edge case、side effect
  2. 记录目标行为,可作为一个文档帮助他人理解代码
  3. 自动化可重复

test包基本使用

https://golang.org/pkg/testing/#pkg-overview

// math/math.go

package math

func Sum(numbers []int) int {
	sum := 0
	for _, n := range numbers {
		sum += n
	}
	return sum
}

// math/math_test.go

package math

import (
	"testing"
)

func TestSum(t *testing.T) {
	sum := Sum([]int{10, -2, 3})
	if sum != 11 {
		t.Errorf("fail want 11 bug got %d", sum)
	}
}

test文件的文件名和函数签名都有格式的要求,并要放在和被测试的函数同一个package下 满足上述要求,运行go test命令就可以看到测试函数的运行结果。

test包实现原理

package sleep

import (
	"fmt"
	"testing"
	"time"
)

func TestTmpExcutable(t *testing.T) {
	t.Log("TestTmpExcutable")
	time.Sleep(2 * time.Minute)
}

20220110005053.png

运行go test -v后可以看到进程中运行着一个临时的二进制文件,它就是由go test命令生成出来的可执行程序,我们可以直接运行该文件。当go test程序执行完成后,该临时文件就会被删除。 可见测试程序没有什么特别之处,我们完全可以不使用该包写出类似检验程序正确性的代码,但该包简化了我们实现的步骤。

test命名约定

  • 文件名以_test.go结尾
  • 函数名格式类似TestXxx
  • 变量名一般使用argwantgot
// 函数名例子
// 函数签名:Test接受者类型_方法名_参数类型
func TestDog_Bark_muzzled(t *testing.T) {

}

// 变量名例子
func TestColor(t *testing.T) {
	arg := "blue"
	want := "#00000FF"
	got := Color(arg)
	if got != want {
		t.Errorf("Color(%q) = %q; want %q", arg, got, want)
	}
}

语言点:

https://golang.org/pkg/fmt/

Integer:
%q	a single-quoted character literal safely escaped with Go syntax.
String and slice of bytes (treated equivalently with these verbs):
%q	a double-quoted string safely escaped with Go syntax

testing.T的方法使用

方法列表

https://golang.org/pkg/testing/#T

  • Fail: 测试失败但继续运行
  • FailNow: 测试失败并停止执行当前goroutine的test
  • Log: 测试失败或者以verbose模式执行test时将内容打印到终端
  • Error: Log + Fail
  • Fatal: Log +FailNow

方法选择

  • 如果后续执行没有意义了,就使用 t.Fatal
  • 如果当前执行的结果与预期不符,但后续操作还可以正常执行,就使用 t.Error ,这样可以更方便看到全局都有什么样的错误信息,而不至于出现了一个小错误就立即终止。

可读的错误信息

  1. 调用了哪个函数使用了什么参数,返回了什么样的错误,我们需要什么样的返回值

    t.Errorf("SomeFunc(%v) err = %v; want %v", func, err, want)
    
  2. 有时如果参数太多,我们也可以酌情选择打印出我们需要的参数

  3. 输出的内容尽可能简洁,易于理解,最重要的是通过错误信息可以方便的复现出整个出错的过程,更好的 debug 。 正如下面的例子只需要seed就可以复现出整个流程,但若只打印出整个数组就不易于理解。

    package random
    
    import (
    	"math/rand"
    	"testing"
    	"time"
    )
    
    func TestPick(t *testing.T) {
    	seed := time.Now().UnixNano()
    	r := rand.New(rand.NewSource(seed))
    	arg := make([]int, 25)
    	for i := 0; i < 25; i++ {
    		arg[i] = r.Int()
    	}
    	got := Pick(arg)
    	for _, v := range arg {
    		if got == v {
    			return
    		}
    	}
    	t.Errorf("Pick(seed=%d) = %d; not in slice", seed, got)
    }
    
    

Example: 可测试的案例代码

Example的好处

如果案例代码未经维护则随着功能代码的更新很容易就失效无法使用,但案例代码若写成可测试的代码,则可保证功能代码更新后运行go test在测试的时候发现问题,保证案例代码的可用性。

Example命名约定

  • 文件名以_test.go结尾

  • 函数名格式func ExampleXX_XX_suffix() { ... }

    // 包级别
    func Example() { ... }
    // 方法级别
    func ExampleF() { ... }
    // 类型级别
    func ExampleT() { ... }
    // T类型的M方法
    func ExampleT_M() { ... }
    
    // 若有多个example函数,可加后缀以示区分。
    func Example_suffix() { ... }
    func ExampleF_suffix() { ... }
    func ExampleT_suffix() { ... }
    func ExampleT_M_suffix() { ... }
    
  • 整个测试文件被识别为example条件:有一个example函数,和至少一个其他的函数、类型、变量或常量,且不含有test或benchmark函数。

Example使用技巧

  • 运行godoc -http=:3000可以在浏览器中看到排版优美的案例代码。

  • 如果因为某些原因(比如在测试interface接口或明确展示出导入了什么包)希望将test文件的全部代码,而不只是example函数,显示在godoc中,则只需要将该example函数抽离出来,单独放在一个文件里即可,不要忘记同时要满足上述命名约定。

  • 使用如下格式,在go test时会检测example函数的输出是否与Output下面的内容相符。

     	// Output:
     	// Hello World
    
  • 使用如下格式,在go test时会检测example函数的输出是否与Unordered output下面的内容相符,但检测顺序是无序的。

    	// Unordered output:
    	// BTC 比特币
    	// ETH 以太坊
    	// DOGE 狗狗币
    

Example小案例

// example/greet.go

// Show how to write testable examples.
package greet

import "fmt"

type Dog struct{}

func (d Dog) Hello() string {
	return fmt.Sprint("woof woof!")
}
// example/greet_test.go

package greet_test

import (
	"fmt"
)

func ExampleDog_Hello_demo() {
	fmt.Println("Dog type w/ demo label")

	// Output:
	// Dog type w/ demo label
}
// example/example_greet_test.go

package greet_test

import (
	"fmt"

	// Needed for initialize side effect
	_ "image/png"
)

var m = make(map[string]string)

func ExampleDog_Hello() {
	m = make(map[string]string)
	m["ETH"] = "以太坊"
	m["BTC"] = "比特币"
	m["DOGE"] = "狗狗币"
	for k, v := range m {
		fmt.Println(k, v)
	}

	// Unordered output:
	// BTC 比特币
	// ETH 以太坊
	// DOGE 狗狗币
}

20220110005130.png

20220110005124.png

类型比较

https://golang.org/ref/spec#Comparison_operators

https://golang.org/pkg/reflect/#DeepEqual

http://www.luffy.vip/post/2020-10-17-golang-comparison/

References

on the list


知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。