20 Go语言——测试

go语言测试

测试的目的是确认目标代码在给定的场景下,有没有按照期望工作 。一个场景是正向路经测试,就是在正常执行的情况下,保证代码不产生错误的测试。
另外一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错误。总之,不管如何调用或者执行代码,所写的代码行为都是可预期的

go语言为我们提供了测试框架testing和自带go test命令来实现单元测试和性能测试。

1、单元测试(unit testing)

go语言自带的testing 框架使用:

  • 测试文件名必须以 _test.go结尾。 比如 main_test.go 或者XXX_test.go
  • 测试用例函数必须以Test开头,首字母大写。 TestXXX 比如TestAdd
  • 测试用例函数的参数必须是 *testing.T 比如 func TestAdd(t *testing.T)()
  • 一个测试文件中可以有多个测试用例函数
  • go test 命令,如果运行正确,无日志,错误时,会输出日志
  • go test -v 命令,运行正确或者错误,都会输出日志
  • go test -v xxx_test.go xxx.go 。。。测试单个测试文件 比如:go test -v main_test.go main.go b.go c.go ,需要制定测试方法所在的文件,有几个写几个
  • 测试单个函数,go test -v -test.run 函数名

1.1 基础单元测试

比如我们有一个函数Sum(a,b int) int 需要测试,

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 有一些方法方便使用

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() 打印自定义信息。

1.2 表组测试

所谓的表组测试,基本上和单元测试一样,只不过它有好几个不同输入以及输出组成的一组单元测试。

简单修改:无非就是多测几次不同的值呗

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

1.3 模仿调用

当我们测试需要网络访问时,我们并没有联网,又不能时时开启服务器,所以这时候模拟网络访问就有必要了。

针对模拟网络访问,标准库了提供了一个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)
}

非常简单,这里是一个/sendjsonAPI,当我们访问这个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())
}

运行这个单元测试,就可以看到我们访问/sendjsonAPI的结果里,并且我们没有启动任何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一样了,打印出结果即可。

1.4 测试覆盖率

就其性质而言,测试不可能是完整的 。再多测试也不能说明程序没有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的文件,双击打开,一样的。

在这里插入图片描述

标记为绿色的代码行已经被测试了;标记为红色的还没有测试到。我们根据没有测试到的代码逻辑,完善我的单元测试代码即可。

1.5 subtests 子单元测试

因为Go 的test 不会保证多个TestXXx 是顺序执行,虽然通常是按顺序执行。 如果我们想要测试一类似工作流顺序执行的时候,我们就用到了subtests ,当然单元测试和基准测试都有相应的子测试。

subtests 的核心是一个叫做 Run() 的方法

func (t *T) Run(name string, f func(t *T)) bool

接收一个string类型的标签合一个测试函数。

比如原来测试代码修改如下,这里只是展示下用法

package main

import (
	"testing"
)

func TestSum(t *testing.T){
    t.Run("顺序1",Sum1)
    t.Run("顺序2",Sum2)
	
}
func Sum1(){
    sum := Sum(1, 2)
	if sum==3{
		t.Logf("成功,结果为%v",sum)
	}else{
		t.Fatalf("有错误,结果为%v",sum)
	}
}
func Sum2(){
    sum := Sum(3, 4)
	if sum==3{
		t.Logf("成功,结果为%v",sum)
	}else{
		t.Fatalf("有错误,结果为%v",sum)
	}
}

可以看到,当调用测试方法是,t.Run()函数依次调用,我们只需要传入需要顺序执行的函数即可,这里只是演示下而已。

1.6 TestMain()

  • 使用TestMain最为初始化test,并且使用m.Run()来调用其他tests可以完成一些需要初始化操作的testing,比如数据库连接,文件打开,rest服务登录等。
  • 如果没有在TestMain中调用m.Run() 则除了TestMain意外的其他tests都不会执行。 也就是如果我们写的整个测试代码中有TestMain()函数,则不调用m.Run()其他的tests都不会执行,如果我们没有写TestMain()函数,那么其他的tests是可以执行的。
func TestMain(m *testing.M)

按需使用,如果我们测试时需要有一些初始化,或者有一些收尾的工作要做,那么我们就用TestMain()

比如:伪代码


func TestMain(m *testing.M){
	clearTables()//自定义的清空所有数据表
	m.Run()
	clearTables()自定义的清空所有数据表
}
//清除表数据
func clearTables(){
	dbConn.Exec("truncate users")
	dbConn.Exec("truncate video_info")
	dbConn.Exec("truncate comments")
	dbConn.Exec("truncate sessions")
}
//subtests,按顺序执行,测试工作流
func TestUserWorkFlow(t *testing.T){
	t.Run("Add",TestAddUser)
	t.Run("Get",TestGetUser)
	t.Run("Del",TestDeleteUser)
	t.Run("Reget",TestRegetUser)
}
//测试往数据库添加
func  TestAddUser(t *testing.T){
	err:=AddUserCredential("admin","123")
	if err!=nil{
		t.Errorf("Error of AddUser: %v",err)
	}
}
//测试从数据库读取
func  TestGetUser(t *testing.T){
	pwd,err:=GetUserCredential("admin")
	if pwd!="123"||err!=nil{
		t.Errorf("Error of GetUser:")
	}
}
//测试从数据库删除
func TestDeleteUser(t *testing.T) {
	err:=DeleteUser("admin","123")
	if err!=nil{
		t.Errorf("error of deleteUser: %v",err )
	}
}
//测试重新读取
func TestRegetUser(t *testing.T){
	pwd, err := GetUserCredential("admin'")
	if err!=nil{
		t.Errorf("error of regetuser:%v",err)
	}
	if pwd!=""{
		t.Errorf("delete user test failed")
	}
}

上面的代码的作用就是,我需要测试数据库的增删改查,所以我需要一些初始化和收尾,我需要测试前清空表,测试完删除测试数据,所以用到了TestMain() ,在testMain()中做了3件事,清空表、进行测试、清空测试数据。

然后我们测试需要顺序执行,添加、查询、删除、测试是否删除成功。就用到了上面的 subtests.

即:t.Run() ,挨个执行。

所以说,我们测试时 遇到需要初始化或收尾工作时,TestMain()还是很有用的。

1.7 t.SkipNow() 跳过当前test

t.SkipNow() 为跳过当前test,并且直接按pass处理下一个test。 注意,这行代码必须卸载test函数的第一行。

func TestPrint(t *testing.T){
    t.SkipNow()
    //测试代码
}

2.基准测试(benchmark testing)

2.1 进行基准测试

基准测试是一种测试代码性能的方法。想要测试解决同一问题的不同方案的性能,以及查看哪种解决方案的性能更好时,基准测试就会很有用。

基准测试也可以用来识别某段代码的 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)
	}
}

这是一个基准测试的例子,从中我们可以看出以下规则:

  1. 基准测试的代码文件必须以_test.go结尾
  2. 基准测试的函数必须以Benchmark开头,必须是可导出的
  3. 基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数
  4. 基准测试函数不能有返回值
  5. b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰
  6. 最后的for循环很重要,被测试的代码要放到循环里
  7. b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能

运行命令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

2.2 性能对比

上面那个基准测试的例子,其实是一个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个字节。 所以效率高低的原因一目了然了。

在代码开发中,对于我们要求性能的地方,编写基准测试非常重要,这有助于我们开发出性能更好的代码。不过性能、可用性、复用性等也要有一个相对的取舍,不能为了追求性能而过度优化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值