// 堆代码 duidaima.com package main import ( "fmt" "math" ) func main() { v := math.Abs(-10) if v != 10 { fmt.Println("测试失败") return } fmt.Println("测试成功") }更常见的可能是,if 判断都没有,直接 Print 输出结果,我们观察结果确认问题。特别对于习惯使用 Python、PHP 脚本语言的开发, 建一个脚本测试是非常快速的,因为曾经很长一段时间,我就是如此。这种方式有什么缺点?我的理解,主要几点,如main 中的测试不容易复用,常常是建了就删;测试用例变多时,灵活性不够,常会有修改代码的需求;自动化测试也不是非常方便等等问题。
package math import ( "math" "testing" ) func TestAbs(t *testing.T) { var a, expect float64 = -10, 10 actual := math.Abs(a) if actual != expect { t.Fatalf("a = %f, actual = %f, expected = %f", a, actual, expect) } }程序非常简洁,a 是 Abs 函数的输入参数,expect 是期望得到的执行结果,actual 是函数执行的实际结果,测试结果由 actual 和 expect 比较结果确定。完成用例编写,go test 命令执行测试,我们会看到如下输出。
$ go test PASS ok study/test/math 0.004s输出为 PASS,表示测试用例成功执行。0.004s 表示用例执行时间。
func TestXxx(*testing.T)测试函数必须按这个固定格式编写,否则 go test 将执行报错。函数中有一个输入参数 t, 类型是 *testing.T,它非常重要,单元测试需通过它反馈测试结果,具体后面再介绍。
// 获取测试名称 method (*T) Name() string // 打印日志 method (*T) Log(args ...interface{}) // 打印日志,支持 Printf 格式化打印 method (*T) Logf(format string, args ...interface{}) // 反馈测试失败,但不退出测试,继续执行 method (*T) Fail() // 反馈测试成功,立刻退出测试 method (*T) FailNow() // 反馈测试失败,打印错误 method (*T) Error(args ...interface{}) // 反馈测试失败,打印错误,支持 Printf 的格式化规则 method (*T) Errorf(format string, args ...interface{}) // 检测是否已经发生过错误 method (*T) Failed() bool // 相当于 Error + FailNow,表示这是非常严重的错误,打印信息结束需立刻退出。 method (*T) Fatal(args ...interface{}) // 相当于 Errorf + FailNow,与 Fatal 类似,区别在于支持 Printf 格式化打印信息; method (*T) Fatalf(format string, args ...interface{}) // 跳出测试,从调用 SkipNow 退出,如果之前有错误依然提示测试报错 method (*T) SkipNow() // 相当于 Log 和 SkipNow 的组合 method (*T) Skip(args ...interface{}) // 与Skip,相当于 Logf 和 SkipNow 的组合,区别在于支持 Printf 格式化打印 method (*T) Skipf(format string, args ...interface{}) // 用于标记调用函数为 helper 函数,打印文件信息或日志,不会追溯该函数。 method (*T) Helper() // 标记测试函数可并行执行,这个并行执行仅仅指的是与其他测试函数并行,相同测试不会并行。 method (*T) Parallel() // 可用于执行子测试 method (*T) Run(name string, f func(t *T)) bool上面列出了单元测试 testing.T 中所有的公开方法,我个人思路,把它们大概分为三类,分别是底层方法、测试反馈,还有一些其他运行控制的辅助方法。基础信息的 API 只有 1 个,Name() 方法,用于获取测试名称。运行控制的辅助方法主要指的是 Helper、t.Parallel 和 Run,上面的注释对它们已经做了简单介绍。
package math import "errors" func Division(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil }Division 非常简单,输入参数 a、b 分别是被除数和除数,输出参数是计算结果和错误提示。如果除数是 0,将会给出相应的错误提示。在正式测试 Division 函数前,我们先要梳理下什么样的输入与期望结果表示测试成功。输入不同,期望结果也就不同,可能是正确结果,亦或者是期待的错误结果。什么意思?以这里的 Division 为例,两种场景需要考虑:
2.期望错误返回结果,当被除数为 10,除数为 0,期望返回除数不能为 0 的错误,即期望返回错误提示;
func TestDivision(t *testing.T) { var a, b, expect float64 = 10, 5, 2 actual, err := Division(a, b) if err != nil { t.Errorf("a = %f, b = %f, expect = %f, err %v", a, b, expect, err) return } if actual != expect { t.Errorf("a = %f, b = %f, expect = %f, actual = %f", a, b, expect, actual) } }定义了三个变量,分别是 a、b、expect,对应被除数、除数和期望结果。用例通过对比 Division 的实际结果 actual 与期望结果 expect 确认测试是否成功。还有就是,Division 返回的 error 也要检查,因为这里期待的正常运行结果,只要有错即可认定测试失败。
func TestDivisionZero(t *testing.T) { var a, b float64 = 10, 0 var expectedErrString = "division by zero" _, err := Division(a, b) if err.Error() != expectedErrString { t.Errorf("a = %f, b = %f, err %v, expect err %s", a, b, err, expectedErrString) return } }同样是首先定义了三个变量,a、b 和 expectErrString,a、b 含义与之前相同,expectErrString 为预期提示的错误信息。除数 b 设置为 0 ,主要是为了测试 Division 函数是否能按预期返回错误,所以我们并不关心计算结果。测试成功与否,通过比较实际的返回 error 与 expectErrString 确定。
$ go test -v === RUN TestDivision --- PASS: TestDivision (0.00s) === RUN TestDivisionZero --- PASS: TestDivisionZero (0.00s) PASS ok study/test/math 0.005s结果显示,测试成功!这个案例的演示中,我们在 go test 上加入 -v 选项,这样就可以清晰地看到每个测试用例的执行情况。
type DivisionTable struct { a float64 // 被除数 b float64 // 除数 expect float64 // 期待计算值 expectErr error // 期待错误字符串 }各字段的含义在注释部分已经做了相关说明,和我们之前做的单个场景的测试涉及字段差不多。区别在于 expectErr 不再是 string 类型。
var table = []DivisionTable{ {1., 1., 1., nil}, {-4., -2., 2., nil}, {2., 0., 7., errors.New("division by zero")}, }简单列举了三种场景,分别正数之间的除法、负数之间的除数以及除数为 0 的情况下的除法。接下来的目标就是实现一个通用 Division 测试函数。直接看代码吧!
func TestDivisionTable(t *testing.T) { for _, v := range divisionTable { actual, err := Division(v.a, v.b) if err == nil { if v.expectErr != nil { t.Errorf( "a = %f, b = %f, actual err not nil, expect err is nil", v.a, v.b) } } else if err != nil { if v.expectErr == nil { t.Errorf( "a = %f, b = %f, actual err not nil, expect err is nil", v.a, v.b) } else if !strings.Contains(err.Error(), v.expectErr.Error()) { t.Errorf( "a = %f, b = %f, actual err = %v, expect err = %v", v.a, v.b, err, v.expectErr) } } else if actual != v.expect { t.Errorf( "a = %f, b = %f, actual = %f, expect = %f", v.a, v.b, actual, v.expect) } } }代码看起来比较乱,这主要是因为 error 接口内部实际类型是指针,不能直接使用比较操作符对比 error,所以要做一些处理。如果没有错误的比较,这个例子就容易理解的多了。
最后一步,比较实际计算结果与期望结果;
func TestDivision(t *testing.T) { var a, b, expect float64 = 10, 5, 2 actual, err := Division(a, b) if err != nil { t.Errorf("a = %f, b = %f, expect = %f, err %v", a, b, expect, err) return } if actual != expect { t.Errorf("a = %f, b = %f, expect = %f, actual = %f", a, b, expect, actual) } t.Log("end") }还是之前的例子,相比之下,最后增加了一段日志打印 t.Log(“end”)。不加任何选项的 go test 的执行效果如下:
$ go test PASS ok study/test/math 0.004s输出日志中并没看到增加那行 end 日志。前面的演示中,我们用到了 go test 的 -v 选项,通过它,可以查看非常详细的输出信息。我们加上 -v 选项,再执行看效果:
$ go test -v === RUN TestDivision --- PASS: TestDivision (0.00s) math_test.go:36: end PASS ok study/test/math 0.005s多出了很多信息,并且打出了那行 end 日志,并给出代码的位置 math_test.go:36: end。除此以外,还具体到了每一个测试的执行情况,比如测试执行开始和测试结果。
$ go test # 目录路径执行 $ go test example/math # GOPATH 包导入路径第二、三场景,执行其中的某个或某类测试,主要与 go test 的 -run 选项有关,-run 选项接收参数是正则表达式。
$ go test -run "^TestDivision$" -v === RUN TestDivision --- PASS: TestDivision (0.00s) math_test.go:36: end PASS ok study/test/math 0.004s从输出中可了解到,确实只执行了 TestDivision。这里要记住加上 -v 选项,使输出信息具体到某一个测试。
$ go test -run "Division" -v === RUN TestDivision --- PASS: TestDivision (0.00s) math_test.go:36: end === RUN TestDivisionZero --- PASS: TestDivisionZero (0.00s) === RUN TestDivisionTable --- PASS: TestDivisionTable (0.00s) PASS ok _/Users/polo/Public/Work/go/src/study/test/math 0.005s将前面写过的函数名中包含 Division 全部执行一遍。第四个场景,执行整个项目下的测试。在项目的顶层目录,直接执行 go test ./… 即可,具体就不演示了。