日常我的项目开发中,单元测试是必不可少的,Go 语言自身就提供了单元测试规范库,很不便咱们发展根底测试,性能测试,事例测试,含糊测试以及剖析代码覆盖率,本篇文章次要介绍 Go 单元测试的基本操作。
单元测试概述
”Go test” 命令可用于运行指定门路下的 Go 文件,这些 Go 文件必须以 “x_test.go” 格局命名,并且测试函数也必须以肯定格局命名。”Go test” 命令编译相干 Go 文件,并运行对应的测试函数,最终输入测试后果,包含测试后果,包名,运行工夫等,而如果执行失败,还会输入具体错误信息。
”Go test” 命令格局能够通过 help 命令查看:
go help test
usage: go test [build/test flags] [packages] [build/test flags & test binary flags] [-args|-json|-c|......]
-args
Pass the remainder of the command line (everything after -args)
-json
Convert test output to JSON suitable for automated processing
-c
Compile the test binary to pkg.test but do not run it
// 等等
咱们举一个简略的根底测试例子,测试取绝对值函数是否正确,如下所示,根底测试函数定义必须为 func TestXxx(* testing.T),包 testing 定义了与单元测试无关的函数或者构造。
func TestAbs(t *testing.T) {got := Abs(-1)
if got != 1 {t.Errorf("Abs(-1) = %d; want 1", got)
}
}
Go 语言都反对哪几种类型的单元测试呢?次要分为根底测试,性能测试,事例测试以及含糊测试,这四种类型测试都有不同的命名格局,同样能够通过 help 命令查看:
go help testfunc
A test function is one named TestXxx (where Xxx does not start with a
lower case letter) and should have the signature,
func TestXxx(t *testing.T) {...}
A benchmark function is one named BenchmarkXxx and should have the signature,
func BenchmarkXxx(b *testing.B) {...}
A fuzz test is one named FuzzXxx and should have the signature,
func FuzzXxx(f *testing.F) {...}
Here is an example of an example:
func ExamplePrintln() {Println("The output of\nthis example.")
// Output: The output of
// this example.
}
根底测试命名格局如 TestXxx,这种类型测试通常用来判断后果是否合乎预期,如果不合乎可应用 t.Errorf 输入起因,也标示着此次测试后果失败;性能测试命名格局如 BenchmarkXxx,将运行指定代码 b.N 次,输入后果蕴含运行次数以及均匀每次耗时;事例测试命名格局如 ExampleXxx,留神这种类型的测试,必须蕴含正文并指定输入 Output,通过比照输入后果,断定测试后果是否通过;含糊测试命名格局如 FuzzXxx,其可通过不通的输出值,验证你代码的正确性。
上面将一一介绍这四种类型的单元测试。
根底测试
根底测试用于验证代码性能正确性,通过比照程序的执行后果分析程序的正确性,应用比较简单,如下是 Go 语言切片的一个根底测试 case:
func TestAppendOverlap(t *testing.T) {x := []byte("1234")
x = append(x[1:], x...) // p > q in runtime·appendslice.
got := string(x)
want := "2341234"
if got != want {t.Errorf("overlap failed: got %q want %q", got, want)
}
}
//ok demo 0.503s
如果执行后果不合乎预期,输入内容是怎么的呢?如下所示:
--- FAIL: TestAppendOverlap (0.00s)
demo_test.go:13: overlap failed: got "xxx" want "xxx"
性能测试
性能测试罕用来剖析比照程序性能,对一段代码,通过执行屡次,计算均匀耗时,以此评估程序性能;执行总工夫或者执行次数可通过参数指定,反对的参数能够通过 help 命令查看:
go help testflag
-bench regexp
Run only those benchmarks matching a regular expression.
By default, no benchmarks are run
// 性能测试执行工夫;留神 Nx 可设置代码段执行循环次数
-benchtime t
Run enough iterations of each benchmark to take t, specified
as a time.Duration (for example, -benchtime 1h30s).
The default is 1 second (1s).
The special syntax Nx means to run the benchmark N times
(for example, -benchtime 100x).
// 性能测试执行次数
-count n
Run each test, benchmark, and fuzz seed n times (default 1).
If -cpu is set, run n times for each GOMAXPROCS value.
Examples are always run once. -count does not apply to
fuzz tests matched by -fuzz.
// 设置逻辑处理器数目,默认为 CPU 核数
-cpu 1,2,4
Specify a list of GOMAXPROCS values for which the tests, benchmarks or
fuzz tests should be executed. The default is the current value
of GOMAXPROCS. -cpu does not apply to fuzz tests matched by -fuzz.
还记得解说字符串的时候提到过,Go 语言字符串是只读的,不能批改的,字符串相加也是通过申请内存与数据拷贝形式实现,如果存在大量的字符串相加逻辑,每次都申请内存拷贝数据效率会十分差;而 stringBuilder 底层保护了一个 []byte,追加字符串只是追加到该切片,最终一次性转换该切片为字符串,防止了两头 N 屡次的内存申请与数据拷贝,所以性能较好。如何通过性能测试验证这个后果呢?
package demo
import (
"strings"
"testing"
)
func BenchmarkStringPlus(b *testing.B) {
s := ""
for i := 0; i < b.N; i++ {s += "abc"}
}
func BenchmarkStringBuilder(b *testing.B) {build := strings.Builder{}
for i := 0; i < b.N; i++ {build.WriteString("abc")
}
}
咱们编写了两个性能测试 case,BenchmarkStringPlus 用于测试字符串相加,BenchmarkStringBuilder 用于测试 stringBuilder;for 循环执行次数为 b.N 次,能够通过参数 benchtime 设置。测试后果如下:
go test -benchtime 100000x -count 3 -bench .
BenchmarkStringPlus-8 100000 15756 ns/op
BenchmarkStringPlus-8 100000 14203 ns/op
BenchmarkStringPlus-8 100000 15751 ns/op
BenchmarkStringBuilder-8 100000 4.148 ns/op
BenchmarkStringBuilder-8 100000 3.663 ns/op
BenchmarkStringBuilder-8 100000 3.372 ns/op
PASS
ok demo 4.686s
//BenchmarkStringPlus-8 示意逻辑处理器 P 数目为 8
如果你的性能测试须要并行执行,能够通过 RunParallel 实现,其会创立多个协程执行你的代码,协程数目默认与逻辑处理器 P 数目保持一致,这种形式的性能测试通常须要联合 -cpu 参数一起应用。上面的 case 用于测试不同并发水平的 sync.Mutex 互斥锁的性能:
package demo
import (
"sync"
"testing"
)
func BenchmarkMutex(b *testing.B) {
var lock sync.Mutex
b.RunParallel(func(pb *testing.PB) {for pb.Next() {lock.Lock()
foo := 0
for i := 0; i < 100; i++ {
foo *= 2
foo /= 2
}
_ = foo
lock.Unlock()}
})
}
测试后果如下:
go test -benchtime 100000x -cpu 1,2,4,8 -bench .
BenchmarkMutex 100000 46.62 ns/op
BenchmarkMutex-2 100000 50.70 ns/op
BenchmarkMutex-4 100000 64.98 ns/op
BenchmarkMutex-8 100000 113.3 ns/op
PASS
ok demo 0.139s
事例测试
事例测试绝对也比较简单,通过在正文并指定输入 Output(如果没有不执行事例测试),通过比照输入后果,断定测试后果是否通过,上面的 case 是一个官网自带的测试用例:
func ExampleSum256() {sum := sha256.Sum256([]byte("hello world\n"))
fmt.Printf("%x", sum)
// Output: a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447
}
一般的 Output 是程序无关的,如果你的输入包含多行,程序必须齐全保持一致;如果不限度输入行程序,能够应用 Unordered output,如上面的事例,输入程序不一样,然而对立能通过测试(如果应用 output,则测试失败)。
package demo
import ("fmt")
func ExampleUnorder() {for _, value := range []int{1,2,3,4,0} {fmt.Println(value)
}
// Unordered output: 4
// 2
// 1
// 3
// 0
}
含糊测试
含糊测试(Fuzzing)是一种通过向指标零碎提供非预期的输出并监督异样后果来发现软件破绽的办法,为什么须要含糊测试呢?因为实践上你不可能穷举所有输出作为测试用例,含糊测试的实质是依附随机函数生成随机测试用例来进行测试验证,是不确定的。实践上只有反复测试的次数足够多,输出足够随机,更容易发现一些偶尔随机谬误,测试后果绝对更牢靠。
与含糊测试相干的几个参数如下所示:
go help testflag
-fuzz regexp
Run the fuzz test matching the regular expression. When specified,
the command line argument must match exactly one package within the
main module, and regexp must match exactly one fuzz test within
that package. Fuzzing will occur after tests, benchmarks, seed corpora
of other fuzz tests, and examples have completed. See the Fuzzing
section of the testing package documentation for details.
// 含糊测试执行工夫;Nx 示意执行多少次
-fuzztime t
Run enough iterations of the fuzz target during fuzzing to take t,
specified as a time.Duration (for example, -fuzztime 1h30s).
The default is to run forever.
The special syntax Nx means to run the fuzz target N times
(for example, -fuzztime 1000x).
......
上面是 Go 一个含糊测试官网事例:
package main
import (
"bytes"
"encoding/hex"
"testing"
)
func FuzzHex(f *testing.F) {
// 增加种子参数
for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {f.Add(seed)
}
f.Fuzz(func(t *testing.T, in []byte) {enc := hex.EncodeToString(in)
out, err := hex.DecodeString(enc)
if err != nil {t.Fatalf("%v: decode: %v", in, err)
}
if !bytes.Equal(in, out) {t.Fatalf("%v: not equal after round trip: %v", in, out)
}
})
}
测试后果如下:
go test -fuzztime 10000x -fuzz=FuzzHex
fuzz: elapsed: 0s, gathering baseline coverage: 0/24 completed
fuzz: elapsed: 0s, gathering baseline coverage: 24/24 completed, now fuzzing with 8 workers
fuzz: elapsed: 0s, execs: 10000 (117451/sec), new interesting: 0 (total: 24)
PASS
ok demo 0.339s
代码覆盖率
代码覆盖率能够用来评估咱们的单元测试覆盖度,帮忙咱们晋升代码品质,而 Go 语言单元测试库自身就反对代码覆盖率剖析(go test -cover)。上面举一个简略的事例。
咱们的代码如下,定义了商品构造,蕴含商品类型以及商品名称,有一个函数可依据商品分类返回商品名称
package demo
type Product struct {
Type int
Name string
}
func ProductName(t int) string{
switch t {
case 1:
return "手机"
case 2:
return "电脑"
case 3:
return "显示器"
case 4:
return "键盘"
default:
return "不晓得"
}
}
测试用例如下:
package demo
import "testing"
var tests = []Product{{1, "手机"},
}
func TestType(t *testing.T) {
for _, p := range tests {name := ProductName(p.Type)
if name != p.Name {t.Errorf("ProductName(%d) = %s; want %s", p.Type, name, p.Name)
}
}
}
简略剖析也能够发现,咱们的测试用例有余,很多分支无法访问到。咱们看下覆盖率剖析后果:
go test -cover
PASS
coverage: 33.3% of statements // 只笼罩了 33.3% 的语句
ok demo 3.049s
总结
日常我的项目开发中,单元测试是必不可少的,本篇文章次要介绍了如何基于 Go 单元测试规范库,实现根本的根底测试,性能测试,事例测试,含糊测试以及剖析代码覆盖率。