测试的目的是确认目标代码在给定的场景下,有没有按照指望工做 。一个场景是正向路经测试,就是在正常执行的状况下,保证代码不产生错误的测试。
另一些单元测试可能会测试负向路径的场景,保证代码不只会产生错误,并且是预期的错误。总之,无论如何调用或者执行代码,所写的代码行为都是可预期的 html
go语言为咱们提供了测试框架testing和自带go test命令来实现单元测试和性能测试。json
go语言自带的testing 框架使用:windows
func TestAdd(t *testing.T)()
好比咱们有一个函数Sum(a,b int) int 须要测试,bash
main.go服务器
package main func Sum(a,b int )int{ return a+b }
main_test.go网络
package main import ( "testing" ) func TestSum(t *testing.T){ sum := Sum(1, 2) if sum==3{ t.Logf("成功,结果为%v",sum) }else{ t.Fatalf("有错误,结果为%v",sum) } }
运行测试命令;并发
//命令行执行 go test 命令 PASS ok _/D_/gopath/src/a_tour_of_go/testing 1.301s //执行go test -v 命令 === RUN TestSum --- PASS: TestSum (0.00s) main_test.go:10: 成功,结果为3 PASS ok _/D_/gopath/src/a_tour_of_go/testing 1.302s //执行 go test -v main_test.go main.go 结果同上,由于咱们就写了一个测试文件。 //执行 go test -v -test.run TestSum 结果同上,由于只有一个函数
其中,*testing.T 有一些方法方便使用app
type T func (c *T) Error(args ...interface{}) func (c *T) Errorf(format string, args ...interface{}) func (c *T) Fail() func (c *T) FailNow() func (c *T) Failed() bool func (c *T) Fatal(args ...interface{}) func (c *T) Fatalf(format string, args ...interface{}) func (c *T) Helper() func (c *T) Log(args ...interface{}) func (c *T) Logf(format string, args ...interface{}) func (c *T) Name() string func (t *T) Parallel() func (t *T) Run(name string, f func(t *T)) bool func (c *T) Skip(args ...interface{}) func (c *T) SkipNow() func (c *T) Skipf(format string, args ...interface{}) func (c *T) Skipped() bool
好比咱们经常使用,Fatalf() 出现错误是调用会打印,而且结束函数, Logf() 打印自定义信息。框架
所谓的表组测试,基本上和单元测试同样,只不过它有好几个不一样输入以及输出组成的一组单元测试。函数
简单修改:无非就是多测几回不一样的值呗
package main import ( "testing" ) func TestSum(t *testing.T){ sum := Sum(1, 2) if sum==3{ t.Logf("成功,结果为%v",sum) }else{ t.Fatalf("有错误,结果为%v",sum) } sum1 := Sum(4, 4) if sum1==8{ t.Logf("成功,结果为%v",sum1) }else{ t.Fatalf("有错误,结果为%v",sum1) } sum2 := Sum(5, 5) if sum2==10{ t.Logf("成功,结果为%v",sum2) }else{ t.Fatalf("有错误,结果为%v",sum2) } } //结果 === RUN TestSum --- PASS: TestSum (0.00s) main_test.go:10: 成功,结果为3 main_test.go:16: 成功,结果为8 main_test.go:22: 成功,结果为10 PASS ok a_tour_of_go/testing 1.253s
当咱们测试须要网络访问时,咱们并无联网,又不能时时开启服务器,因此这时候模拟网络访问就有必要了。
针对模拟网络访问,标准库了提供了一个httptest
包,可让咱们模拟http的网络调用。
首先咱们建立一个处理HTTP请求的函数,并注册路由
package common import ( "net/http" "encoding/json" ) func Routes(){ http.HandleFunc("/sendjson",SendJSON) } func SendJSON(rw http.ResponseWriter,r *http.Request){ u := struct { Name string }{ Name:"张三", } rw.Header().Set("Content-Type","application/json") rw.WriteHeader(http.StatusOK) json.NewEncoder(rw).Encode(u) }
很是简单,这里是一个/sendjson
API,当咱们访问这个API时,会返回一个JSON字符串。如今咱们对这个API服务进行测试,可是咱们又不能时时刻刻都启动着服务,因此这里就用到了外部终端对API的网络访问请求。
func init() { common.Routes() } func TestSendJSON(t *testing.T){ req,err:=http.NewRequest(http.MethodGet,"/sendjson",nil) if err!=nil { t.Fatal("建立Request失败") } rw:=httptest.NewRecorder() http.DefaultServeMux.ServeHTTP(rw,req) log.Println("code:",rw.Code) log.Println("body:",rw.Body.String()) }
运行这个单元测试,就能够看到咱们访问/sendjson
API的结果里,而且咱们没有启动任何HTTP服务就达到了目的。这个主要利用httptest.NewRecorder()
建立一个http.ResponseWriter
,模拟了真实服务端的响应,这种响应时经过调用http.DefaultServeMux.ServeHTTP
方法触发的。
还有一个模拟调用的方式,是真的在测试机上模拟一个服务器,而后进行调用测试。
func mockServer() *httptest.Server { //API调用处理函数 sendJson := func(rw http.ResponseWriter, r *http.Request) { u := struct { Name string }{ Name: "张三", } rw.Header().Set("Content-Type", "application/json") rw.WriteHeader(http.StatusOK) json.NewEncoder(rw).Encode(u) } //适配器转换 return httptest.NewServer(http.HandlerFunc(sendJson)) } func TestSendJSON(t *testing.T) { //建立一个模拟的服务器 server := mockServer() defer server.Close() //Get请求发往模拟服务器的地址 resq, err := http.Get(server.URL) if err != nil { t.Fatal("建立Get失败") } defer resq.Body.Close() log.Println("code:", resq.StatusCode) json, err := ioutil.ReadAll(resq.Body) if err != nil { log.Fatal(err) } log.Printf("body:%s\n", json) }
模拟服务器的建立使用的是httptest.NewServer
函数,它接收一个http.Handler
处理API请求的接口。 代码示例中使用了Hander的适配器模式,http.HandlerFunc
是一个函数类型,实现了http.Handler
接口,这里是强制类型转换,不是函数的调用。
这个建立的模拟服务器,监听的是本机IP127.0.0.1
,端口是随机的。接着咱们发送Get请求的时候,再也不发往/sendjson
,而是模拟服务器的地址server.URL
,剩下的就和访问正常的URL同样了,打印出结果便可。
就其性质而言,测试不多是完整的 。再多测试也不能说明程序没有bug,测试能够加强咱们的信心,让咱们的程序在一个放心的环境中正常运行。
由单元测试的代码,触发运行到的被测试代码的代码行数占全部代码行数的比例,被称为测试覆盖率,代码覆盖率不必定彻底精准,可是能够做为参考,能够帮咱们测量和咱们预计的覆盖率之间的差距,go test
工具,就为咱们提供了这么一个度量测试覆盖率的能力。
简单来讲就是一个参数 - coverprofile
好比:
main.go
package main import "fmt" func Tag(tag int){ switch tag { case 1: fmt.Println("Android") case 2: fmt.Println("Go") case 3: fmt.Println("Java") default: fmt.Println("C") } }
main_test.go
package main import ( "testing" ) func TestTag(t *testing.T) { Tag(1) Tag(2) }
执行命令: go test -v -coverprofile=c.out
输出结果:
=== RUN TestTag
Android
Go
--- PASS: TestTag (0.00s)
PASS
coverage: 60.0% of statements
ok a_tour_of_go/testing 1.309s
获得测试覆盖率为60% , 咱们以前的c.out 是生成的测试报告,咱们能够看到当前目录下有一个c.out文件
咱们能够生成 html 文件 go tool cover -html=c.out
会直接打开一个网页,显示咱们的代码,经过颜色区分。 固然也能够go tool cover -html=c.out -o=tag.html
在当前目录生成一个名为tag.html的文件,双击打开,同样的。
标记为绿色的代码行已经被测试了;标记为红色的尚未测试到。咱们根据没有测试到的代码逻辑,完善个人单元测试代码便可。
基准测试是一种测试代码性能的方法。想要测试解决同一问题的不一样方案的性能,以及查看哪一种解决方案的性能更好时,基准测试就会颇有用。
基准测试也能够用来识别某段代码的 CPU或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。许多开发人员会用基准测试来测试不一样的并发模式,或者用基准测试来辅助配置工做池的数量,以保证能最大化系统的吞吐量。
main_test.go
package main import ( "fmt" "testing" ) func BenchmarkSprintf(b *testing.B){ num:=10 b.ResetTimer()//重置计时器 for i:=0;i<b.N;i++{ fmt.Sprintf("%d",num) } }
这是一个基准测试的例子,从中咱们能够看出如下规则:
b.ResetTimer
是重置计时器,这样能够避免for循环以前的初始化代码的干扰运行命令go test -v -run=none -bench=.
查看结果
goos: windows goarch: amd64 BenchmarkSprintf-8 10000000 134 ns/op PASS ok _/D_/gopath/src/a_tour_of_go/testing 2.797s
-bench
:是进行基准测试参数, =. 表示全部的函数,若是要特定函数只须要后面跟函数名,好比 -bench=BenchmarkSprintf
-run=none
:的做用是,运行一个none 不存在的单元测试,避免单元测试输出干扰。由于运行基准测试的时候是默认运行咱们的单元测试的。咱们为了查看方便,就运行一个不存在的单元测试过滤掉。
输出结果表示:
看到函数后面的-8
了吗?这个表示运行时对应的GOMAXPROCS的值。接着的10000000
表示运行for循环的次数,也就是调用被测试代码的次数,最后的134 ns/op
表示每次须要话费134纳秒。
以上是测试时间默认是1秒,也就是1秒的时间,调用1000万次,每次调用花费134纳秒。若是想让测试运行的时间更长,能够经过-benchtime
指定,好比3秒。
go test -bench=. -benchtime=3s -run=none
goos: windows goarch: amd64 BenchmarkSprintf-8 30000000 121 ns/op PASS ok _/D_/gopath/src/a_tour_of_go/testing 5.731s
上面那个基准测试的例子,实际上是一个int类型转为string类型的例子,标准库里还有几种方法,咱们看下哪一种性能更加。
func BenchmarkSprintf(b *testing.B){ num:=10 b.ResetTimer() for i:=0;i<b.N;i++{ fmt.Sprintf("%d",num) } } func BenchmarkFormat(b *testing.B){ num:=int64(10) b.ResetTimer() for i:=0;i<b.N;i++{ strconv.FormatInt(num,10) } } func BenchmarkItoa(b *testing.B){ num:=10 b.ResetTimer() for i:=0;i<b.N;i++{ strconv.Itoa(num) } }
运行基准测试,看看结果
运行命令: go test -bench=. -run=none goos: windows goarch: amd64 BenchmarkSprintf-8 10000000 126 ns/op BenchmarkFormat-8 300000000 3.85 ns/op BenchmarkItoa-8 300000000 3.93 ns/op PASS ok _/D_/gopath/src/a_tour_of_go/testing 5.893s
从结果上看strconv.FormatInt
函数是最快的,其次是strconv.Itoa
,而后是fmt.Sprintf
最慢。第一个最慢,咱们能够经过-benchmem
找到根本缘由。
运行命令: go test -bench=. -benchmem -run=none goos: windows goarch: amd64 BenchmarkSprintf-8 10000000 132 ns/op 16 B/op 2 allocs/op BenchmarkFormat-8 300000000 3.97 ns/op 0 B/op 0 allocs/op BenchmarkItoa-8 300000000 4.25 ns/op 0 B/op 0 allocs/op PASS ok _/D_/gopath/src/a_tour_of_go/testing 6.073s
-benchmem
能够提供每次操做分配内存的次数,以及每次操做分配的字节数。结果显示,效率高的两个每次操做进行0次内存分配,每次分配操做0个字节,多是这个太简单了,因此没来得及分配。慢的那个每次操做进行2次内存分配,每次16个字节。 因此效率高低的缘由一目了然了。
在代码开发中,对于咱们要求性能的地方,编写基准测试很是重要,这有助于咱们开发出性能更好的代码。不过性能、可用性、复用性等也要有一个相对的取舍,不能为了追求性能而过分优化。