Golang单元测试与断言编写流程详解
目录
- 编写单元测试
- 批量测试(test tables)
- 执行测试
- 性能测试
- 配置计算时间
- 断言(assertion)
Go 在testing包中内置测试命令go test
,提供了最小化但完整的测试体验。标准工具链还包括基准测试和基于代码覆盖的语句,类似于NCover(.NET)或Istanbul(Node.js)。本文详细讲解go编写单元测试的过程,包括性能测试及测试工具的使用,另外还介绍第三方断言库的使用。
编写单元测试
go中单元测试与语言中其他特性一样具有独特见解,如格式化、命名规范。语法有意避免使用断言,并将检查值和行为的责任留给开发人员。
下面通过示例进行说明。我们编写Sum函数,实现数据求和功能:
package main func Sum(x int, y int) int { return x + y } func main() { Sum(5, 5) }
然后在单独的文件中编写测试代码,测试文件可以在相同包中,或不同包中。测试代码如下:
package main import "testing" func TestSum(t *testing.T) { total := Sum(5, 5) if total != 10 { t.Errorf("Sum was incorrect, got: %d, want: %d.", total, 10) } }
Golang测试功能特性:
- 仅需要一个参数,必须是
t *testing.T
- 以Test开头,接着单词或词组,采用骆驼命名法,举例:TestValidateClient
- 调用
t.Error
或t.Fail
表明失败(当然也可以使用t.Errorf
提供更多细节) t.Log
用于提供非失败的debug信息输出- 测试文件必须命名为
something_test.go
,举例:addition_test.go
批量测试(test tables)
test tables
概念是一组(slice数组)测试输入、输出值:
func TestSum(t *testing.T) { tables := []struct { x int y int n int }{ {1, 1, 2}, {1, 2, 3}, {2, 2, 4}, {5, 2, 7}, } for _, table := range tables { total := Sum(table.x, table.y) if total != table.n { t.Errorf("Sum of (%d+%d) was incorrect, got: %d, want: %d.", table.x, table.y, total, table.n) } } }
如果需要触发错误,我们可以修改测试数据,或修改代码。这里修改代码return x*y
, 输出如下:
=== RUN TestSum
math_test.go:61: Sum of (1+1) was incorrect, got: 1, want: 2.
math_test.go:61: Sum of (1+2) was incorrect, got: 2, want: 3.
math_test.go:61: Sum of (5+2) was incorrect, got: 10, want: 7.
--- FAIL: TestSum (0.00s)FAIL
单元测试不仅要正向测试,更要进行负向测试。
执行测试
执行测试有两种方法:
在相同目录下运行命令:
go test
这会匹配任何packagename_test.go的任何文件。
使用完整的包名
go test
现在我们可以运行单元测试了,还可以增加参数go test -v
获得更多输出结果。
单元测试和集成测试的区别在于单元测试通常不依赖网络、磁盘等,仅测试一个功能,如函数。
另外还可以查看测试语句覆盖率,增加-cover
选项。但高覆盖率未必总是比低覆盖率好,关键是功能正确。
如果执行下面命令,可以生成html文件,以可视化方式查看覆盖率:
go test -cover -coverprofile=c.out
go tool cover -html=c.out -o coverage.html
性能测试
benchmark 测试衡量程序性能,可以比较不同实现差异,理解影响性能原因。
go性能测试也有一定规范:
性能测试函数名必须以Benchmark
开头,之后大写字母或下划线。因此BenchmarkFunctionName()
和 Benchmark_functionName()
都是合法的,但Benchmarkfunctionname()
不合法。这与单元测试以Test开头规则一致。
虽然可以把单元测试和性能测试代码放在相同文件,但尽量避免,文件命名仍然以_test.go结尾。如单元测试文件为simple_test.go,性能测试为benchmark_test.go。
下面通过示例进行说明,首先定义函数:
func IsPalindrome(s string) bool { for i := range s { if s[i] != s[len(s)-1-i] { return false } } return true }
先编写单元测试,分别编写正向测试和负向测试:
func TestPalindrome(t *testing.T) { if !IsPalindrome("detartrated") { t.Error(`IsPalindrome("detartrated") = false`) } if !IsPalindrome("kayak") { t.Error(`IsPalindrome("kayak") = false`) } } func TestNonPalindrome(t *testing.T) { if IsPalindrome("palindrome") { t.Error(`IsPalindrome("palindrome") = true`) } }
接着编写基准测试(性能测试):
func BenchmarkIsPalindrome(b *testing.B) { for i := 0; i < b.N; i++ { IsPalindrome("A man, a plan, a canal: Panama") } }
执行性能测试
go test -bench . -run notest
-bench参数执行所有性能测试,也可以使用正则代替.
,默认情况单元测试也会执行,因为单元测试种有错误,可以通过-run 参数指定值不匹配任何测试函数名称,从而仅执行性能测试。
我们还可以指定其他参数,下面示例指定count为2,表示对现有测试执行两次分析。设置GOMAXPROCS为4,查看测试的内存情况,执行这些请求时间为2秒,而不是默认的1秒执行时间。命令如下:
$ go test -bench=. -benchtime 2s -count 2 -benchmem -cpu 4 -run notest
goos: windows
goarch: amd64
pkg: gin01/math
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
BenchmarkIsPalindrome
BenchmarkIsPalindrome-4 1000000000 1.349 ns/op 0 B/op 0 allocs/op
BenchmarkIsPalindrome-4 1000000000 1.356 ns/op 0 B/op 0 allocs/op
PASS
ok gin01/math 3.234s
-4
: 执行测试的GOMAXPROCS数量1000000000
:为收集必要数据而运行的次数1.349 ns/op
:测试每个循环执行速度PASS
:指示基准测试运行的结束状态。
配置计算时间
定义函数:
func sortAndTotal(vals []int) (sorted []int, total int) { sorted = make([]int, len(vals)) copy(sorted, vals) sort.Ints(sorted) for _, val := range sorted { total += val total++ } return }
对应单元测试如下:
func BenchmarkSort(b *testing.B) { rand.Seed(time.Now().UnixNano()) size := 250 data := make([]int, size) for i := 0; i < b.N; i++ { for j := 0; j < size; j++ { data[j] = rand.Int() } sortAndTotal(data) } }
每次执行前,随机生成数组,造成性能测试不准确。
为了更准确计算时间,可以使用下面函数进行控制:
-StopTimer() : 停止计时器方法.
-StartTimer() : 启动计时器方法.
-ResetTimer() : 重置计时器方法.
最终性能测试函数如下:
func BenchmarkSort(b *testing.B) { rand.Seed(time.Now().UnixNano()) size := 250 data := make([]int, size) // 开始前先重置 b.ResetTimer() for i := 0; i < b.N; i++ { // 准备数据时停止计时 b.StopTimer() for j := 0; j < size; j++ { data[j] = rand.Int() } // 调用函数时启动计时 b.StartTimer() sortAndTotal(data) } }
断言(assertion)
go测试没有提供断言,对于java开发人员来说有点不习惯。这里介绍第三方库 github.com/stretchr/testify/assert
.它提供了一组易理解的测试工具。
assert示例
assert子库提供了便捷的断言函数,可以大大简化测试代码的编写。总的来说,它将之前需要判断 + 信息输出的模式:
import ( "testing" "github.com/stretchr/testify/assert" ) func TestSomething(t *testing.T) { var a string = "Hello" var b string = "Hello" assert.Equal(t, a, b, "The two words should be the same.") }
观察到上面的断言都是以TestingT为第一个参数,需要大量使用时比较麻烦。testify提供了一种方便的方式。先以testing.T创建一个Assertions对象,Assertions定义了前面所有的断言方法,只是不需要再传入TestingT参数了。
func TestEqual(t *testing.T) { assertions := assert.New(t) assertion.Equal(a, b, "") // ... }
TestingT类型定义如下,就是对*testing.T做了一个简单的包装:
// TestingT is an interface wrapper around *testing.T type TestingT interface { Errorf(format string, args ...interface{}) }
下面引用官网的一个示例。
首先定义功能函数Addition:
func Addition(a, b int) int { return a + b }
测试代码:
import ( "github.com/stretchr/testify/assert" "testing" ) // 定义比较函数类型,方便后面批量准备测试数据 type ComparisonAssertionFunc func(assert.TestingT, interface{}, interface{}, ...interface{}) bool // 测试参数类型 type args struct { x int y int } func TestAddition(t *testing.T) { tests := []struct { name string args args expect int assertion ComparisonAssertionFunc }{ {"2+2=4", args{2, 2}, 4, assert.Equal}, {"2+2!=5", args{2, 2}, 5, assert.NotEqual}, {"2+3==5", args{2, 3}, 5, assert.Exactly}, } for _, tt := range tests { // 动态执行断言函数 t.Run(tt.name, func(t *testing.T) { tt.assertion(t, tt.expect, Addition(tt.args.x, tt.args.y)) }) } assert.Equal(t, 2, Addition(1, 1), "sum result is equal") }
到此这篇关于Golang单元测试与断言编写流程详解的文章就介绍到这了,更多相关Go单元测试内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!