Golang测试框架调研
1 Testing框架
1.1 框架介绍
// 设置代理并打开GO111MODULE 下载github包不被墙
// go env -w GOPROXY=https://goproxy.cn,direct
// go env -w GO111MODULE=on 必须打开
// go mod init 初始化项目用go mod管理,之后 用go get后自动关联
import “testing”
//测试文件以xx_test.go命名, 通常和被测试文件(xx)放在同一个包中,
//被测试函数名以Test开头,可导出公开函数
//单元测试:验证返回结果是否预期
func TestSum(t *testing.T){
}
go test –v main_test.go main.go
//性能测试:查看方法瓶颈,耗时,以及其它衡量指标(可查)
func BenchmarkSum(b *testing.B) {
}
go test –v –bench=“BenchmarkSum$” –run=none –cpuprifile cpu.out main_test.go main.go
go tool pprof cpu.out //用于分析上面导出的cpu.out文件
go test -v -cover //-v表示显示测试详细信息,-cover表示在终端查看覆盖率
go test -v ... //用三个点来测试整个项目
go tool cover -html=cover.out -o coverage.html //生成HTML格式的覆盖率详细信息
-------------------------------------------------------
执行多文件的文件夹中单个文件测试时要带上源文件
go test -v xx_test.go xx.go
----------------------------本地生成查看测试覆盖率--------------------
对当前生成覆盖率
go test ./... -gcflags=-l -v -coverprofile=profile.cov
查看该文件
go tool cover -html=profile //注意没有后缀,会弹出到浏览器查看覆盖率
2 GoConvey 框架
2.1 框架介绍(主要用来组织测试用例)
/*
拉取项目后都要执行下面的安装
gitlab上的go版本和本地不同要执行
1 go mod vendor
安装 2 go get -d github.com/smartystreets/goconvey
官网 http://goconvey.co/
So(,) 断言函数条件列表:
https://github.com/smartystreets/goconvey/wiki/Assertions
组织测试用例的,提供了很多断言
直接与 go test 集成
巨大的回归测试套件
可读性强的色彩控制台输出
完全自动化的 Web UI
测试代码生成器
桌面提醒(可选)
自动在终端中运行自动测试脚本
可立即在 Sublime Text 中打开测试问题对应的代码行 (some assembly required)
*/
//测试模块
//外侧集成go test 的基础上使用Convey +So()的形式执行测试用例
func TestAdd(t *testing.T){//比如对Add()函数进行测试
//每一个Convey()相当于一个测试用例,并且Convey可以嵌套Convey
Convey("用来标记当前这个测试的提示信息",t,func(){
//Convey函数有三个参数分别为 提示信息、传入的testing、无返回值的闭包函数
//当前在闭包函数语句块内
//首先是被测函数传递参数的初始化
a :=1
b :=2
//初始化完毕,调用So()进行测试,
//So(,,)的第一个参数为被测函数,第二个为断言函数,第三个为可选的和被测函数结果进行比对
So(Add(),断言函数,)
})
}
2.2 框架实践
/// --------------------------demo-------------------------
./model|
gofun.go //待测试文件
gofun_test.go //使用goconvey自带断言函数测试
------------------------------------------------------------
//gofun.go
package model
import (
"errors"
)
func Add(a, b int) int {
return a + b
}
func Subtract(a, b int) int {
return a - b
}
func Multiply(a, b int) int {
return a * b
}
func Division(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("被除数不能为 0")
}
return a / b, nil
}
----------------------------------------------------------------------
//gofun_test.go
package model
import (
"testing"
. "github.com/smartystreets/goconvey/convey" // 前面加.是省略包前缀
)
//用自带的断言函数
func TestCase1(t *testing.T){
//当前测试用例有两组一共三个测试,要都通过才能全部通过,有一个不通过也是失败
Convey("两数相减", t, func() {
So(Add(1, 2), ShouldEqual, 3)
})
//Convey的嵌套使用
Convey("测试多个用例", t, func() {
Convey("两数相加", func() {
So(Add(1, 2), ShouldEqual, 3)
})
Convey("除数不为0", func() {
_, err := Division(3, 0)
So(err, ShouldNotBeNil) //返回nil测试通过
})
})
}
//断言函数原型
//func So(actual interface{}, assert assertion, expected ...interface{})
//type assertion func(actual interface{}, expected ...interface{}) string
/*
const (
//
success = ""
needExactValues = "This assertion requires exactly %d comparison values (you provided %d)."
needNonEmptyCollection = "This assertion requires at least 1 comparison value (you provided 0)."
)
*/
//定制断言函数
func TestCase2(t *testing.T){
Convey("定制断言判断两数相加",t,func(){
So(Add(6,2),ShouldEqualSelf,8)
})
}
func ShouldEqualSelf(actual interface{}, expected ...interface{}) string{
if actual == expected[0]{ //比较两个空接口的值
return ""
}
return "The number is Notequal!"
}
//-------------------------------------命令行运行-----------------------
//在当前测试包所在目录下 go test -v
/*
--------------------web网页测试-------------------------------
1 在vscode中将ssh终端从cmd切换到bash
2 当前测试包执行 go test -v
3 然后执行 $GOPATH/bin/goconvey
4 浏览器输入网址 localhost:8080 查看当前测试
*/
3 GoStub 框架
3.1 框架介绍(主要用来对函数、全局变量打桩)
/*
description
GoStub框架,接口友好,可以对全局变量、函数或过程打桩(桩是未实现的关联函数,提供一个返回值)
给函数打桩时,需要做侵入式修改
实现测试中的隔离(A-B-C)、补齐(未实现的函数)、控制
安装 go get -d github.com/prashantv/gostub
*/
3.2 框架实践
/// --------------------------demo1--------------------
//---------------------为全局变量、函数、过程打桩--------------------
package main
import (
"fmt"
."github.com/prashantv/gostub" //gostub.Stub()在前面加.号后直接Stub()
)
var num = 200
//1.打桩全局变量
func stubGlobalVar(){
// 通过测试过程的打桩,在测试的时候使用的值测试完毕恢复,不影响原来的定义
// 也就是测试全局变量的值设定
// Stub(para1,para2) //para1是变量指针,para2是桩值(具体的值,或者匿名函数)
fmt.Println(num) //输出200
stubs := Stub(&num,100)
defer stubs.Reset() // 函数运行完毕恢复全局变S量的值
fmt.Println(num) //输出100
stubs.Stub(&num,300) //可以多次打桩,打桩改变桩值
fmt.Println(num) //输出 300
}
//2.打桩函数,库函数也是如此打桩
func ComplexAdd(a int,b int) int{
//假设本函数的加法有更复杂的实现但还没有实现,测试的时候指定最简单的一种加法来打桩
return a*10 +b*100
}
func stubComplexAdd(){
//Stub()的第一个参数必须是指针变量,因此先建立函数变量,然后传递,不能直接传函数
// 错位示范 Stub(&ComplexAdd,闭包或者匿名函数)其中第一个参数错误
fmt.Println(ComplexAdd(1,2)) //输出210
var ComplexAddFun = ComplexAdd //函数变量ComplexAddFun,原函数ComplexAdd
stubs := Stub(&ComplexAddFun,func(a int,b int)int{
//该闭包函数的函数头和返回值要按照被打桩的函数格式
return a+b //只是预先设定了ComplexAdd的返回值
})
//在现在的定义后,ComplexAdd的函数内容已经被打桩实现
defer stubs.Reset() //stubComplexAdd()调用完毕恢复原函数
fmt.Println(ComplexAdd(1,2)) //输出210 ,打桩只是给函数变量打桩,原函数没变
fmt.Println(ComplexAddFun(1,2)) //输出3
//StubFunc(&函数变量,一个固定的返回值) 将原本的函数实现为返回固定值
stubs.StubFunc(&ComplexAddFun,10)
fmt.Println(ComplexAddFun(1,2)) //10
}
//3.为过程打桩,过程就是无返回值的函数
func Process(str string){
fmt.Printf("你好"+str)
}
func stubProcess(){
var ProcessFun = Process
stubs := StubFunc(&ProcessFun)
defer stubs.Reset()
ProcessFun("hhh")
}
//无论是Stub还是StubFunc都会返回stubs对象,这个对象还可以调用Stub和StubFunc方法
//也就是一个测试用例中的stubs对象可以一直使用
func main(){
stubGlobalVar()
stubComplexAdd()
}
/// --------------------------demo2--------------------
//---------------------Stub和Convey的结合使用--------------------
// 测试被测文件中的全局变量、桩函数、库函数的使用
model_01|
stubUse.go
stubUse_test.go
-----------------------------------------------------------------
//stubUse.go
package model_01
import (
"time"
)
var Num = 100 //打桩来测试全局变量
func ComplexAdd(a int,b int)int{
return a*10+b*100 //一种复杂的加法运算,假设暂时未完全实现,用打桩来测试连接
}
------------------------------------------------------------------
//stubUse_test.go
package model_01
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
."github.com/prashantv/gostub"
)
func TestAll(t *testing.T){
Convey("convey和stub联合",t,func(){
Convey("打桩全局变量",func(){
//无参,无返回值的闭包
stubs := Stub(&Num,100)
defer stubs.Reset()
So(Num,ShouldEqual,100)
})
Convey("打桩函数",func(){
var ComplexAddFunc = ComplexAdd //定义函数变量
stubs := Stub(&ComplexAddFunc,func(a int,b int)int{
return a+b
})
So(ComplexAddFunc(1,2),ShouldEqual,3)
})
Convey("非打桩函数", func() {
So(ComplexAdd(1, 2), ShouldEqual, 3)
})
})
}
4 GoMock 框架
4.1 框架介绍(主要用来对接口打桩)
/*
description
GoMock 主要用来给接口打桩的。 mockgen 可以生成对应的接口测试文件
// 安装两个工具
// go get github.com/golang/mock/gomock 库
// go get github.com/golang/mock/mockgen 代码辅助生产工具
mockgen有两种操作模式:源文件和反射
(下面的xxx.go主要是定义接口的go代码)
mockgen -source=xxxx.go > mock/mock_xxxx.go
后半句重定向不指定的话会自动生成该文件中接口的名字
反射模式通过构建一个程序用反射理解接口生成一个mock类文件,通过两个非标志参数开启:导入路径和用逗号 分隔的符号列表(多个interface)
mockgen packagepath Interface1,Interface2...
第一个参数是基于GOPATH的相对路径,第二个参数可以为多个interface,并且interface之间只能用逗号分 隔,不能有空格
对于复杂场景,如一个源文件定义了多个interface而只想对部分interface进行mock,或者interface存在 嵌套,则需要使用反射模式
*/
4.2 框架实践
/// --------------------------demo1 用源文件的方式生成gomock--------------------
hellomock|
hellomock.go//这个就是接口类,要用gomock来测试
person.go //Person实现了上面的接口
Company.go //有方法的参数是hellomock.go的接口
----------------------------------------------------------------------
//hellomock.go
package hellomock
type Talker interface { //对该文件的接口及接口里的函数进行mock
SayHello(word string) (response string)
}
--------------------------------------------------------------------
//person.go
package hellomock
import "fmt"
type Person struct { //实现了接口
name string
}
func NewPerson(name string) *Person {
return &Person{name: name}
}
func (p *Person) SayHello(name string) (word string) {
return fmt.Sprintf("hello %s, %s", name, p.name)
}
-------------------------------------------------------------------
//Company.go
package hellomock
type Company struct {
Usher Talker
}
func NewCompany(t Talker) *Company {
return &Company{
Usher: t,
}
}
func (c *Company) Meeting(guestName string) string {
return c.Usher.SayHello(guestName)
}
-------------------------------------------------------------------
//普通的测试方式 需要先生成一个真实的对象
func TestNormal(t *testing.T) {
c := NewPerson("xiaobai") //c实现了接口
company := NewCompany(c) //NewCompany的参数是接口类
t.Log(company.Meeting("说话"))
}
------------------------------------------------------------------
/*
用gomock的工具mockgen实现测试
1.在bash中先生成mock对象
$GOPATH/bin/mockgen -souce=hellomock.go > mock/mock_Talker.go
2.使用mock_Talker进行gomock
*/
5 Monkey 框架
5.1 框架介绍(主要用来给成员函数也就是方法打桩的)
//参考网站 (两个都要看)
// https://www.jianshu.com/p/2f675d5e334e
// https://www.jianshu.com/p/633b55d73ddd
安装
go get -d github.com/agiledragon/gomonkey
go mod vendor
支持为一个函数打一个桩
支持为一个成员方法打一个桩
支持为一个全局变量打一个桩
支持为一个函数变量打一个桩
支持为一个函数打一个特定的桩序列
支持为一个成员方法打一个特定的桩序列
支持为一个函数变量打一个特定的桩序列
对于被测函数如果只有一条语句,会出现inline情况,打桩失败,要在终端调用语句中加入 -gcflags=-l禁止inline
go test -gcflags=-l -v xx_test.go xx.go
打桩目标是否为内联的函数或成员方法?如果是,请在测试时通过命令行参数 -gcflags=-l (go1.10 版本之前)或-gcflags=all=-l(go1.10 版本及之后)关闭内联优化;
对于golang版本在1.6及以上,如果被测函数首字母小写会报panic,做法是不对非公有函数测试
//---------------------函数API--------------------------------------
//1 为函数打桩,ApplyFunc 第一个参数是函数名,第二个参数是桩函数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。
func ApplyFunc(target, double interface{}) *Patches
func (this *Patches) ApplyFunc(target, double interface{}) *Patches
//2 为方法打桩,ApplyMethod 第一个参数是目标类的指针变量的反射类型,第二个参数是字符串形式的方法名,第三个参数是桩函数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。
//比如Person类的Add方法打桩
// var p *Person :ApplyMethod(reflect.TypeOf(p), "Add", 匿名装函数)
func ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches
func (this *Patches) ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches
//3 为全局变量打桩,ApplyGlobalVar 第一个参数是全局变量的地址,第二个参数是全局变量的桩。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。
func ApplyGlobalVar(target, double interface{}) *Patches //注意target传地址
func (this *Patches) ApplyGlobalVar(target, double interface{}) *Patches
//4 为函数变量打桩,ApplyFuncVar 第一个参数是函数变量的地址,第二个参数是桩函数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。
func ApplyFuncVar(target, double interface{}) *Patches
func (this *Patches) ApplyFuncVar(target, double interface{}) *Patches
5.2 框架实践
/// --------------------------demo--------------------
//--------------------sona1.go----------------------------------------------------
package sonar
var Num = 200 //打桩来测试全局变量
func ComplexAdd(a int, b int) int {
return a10 + b100 //一种复杂的加法运算,假设暂时未完全实现,用打桩来测试连接
}
func AAA(a, b int) int {
return ComplexAdd(a, b)
}
//-----------------sonar1_test.go---------------------------------------------
package sonar
import (
“fmt”
“testing”
. “github.com/agiledragon/gomonkey”
. “github.com/smartystreets/goconvey/convey”
)
func TestAll(t *testing.T) {
Convey(“convey和monkey联合”, t, func() {
Convey(“打桩全局变量\n”,func(){
patches := ApplyGlobalVar(&WorkQueues, 100)
defer patches.Reset()
fmt.Println(num) //输出num为100
})
Convey("打桩函数\n", func() {
fmt.Println(AAA(1, 2)) //这里是原函数输出 210
patches := ApplyFunc(ComplexAdd, func(a int, b int) int { //原函数前面不加&
return a + b
})
defer patches.Reset()
//现在对ComplexAdd函数打桩
fmt.Println(AAA(1, 2)) //这里是打桩输出 3
So(AAA(1, 2), ShouldEqual, 3)
})
})
}
//-----------------------------在当前包下执行指令-----------------------------
- 注意ComplexAdd函数只有一行语句,打桩要加-gcflags=-l,否则打桩失败
go test -gcflags=-l -v sonar1_test.go
## 6.sonar 加入测试覆盖率
### 6.1相关网站
https://docs.sonarqube.org/latest/analysis/coverage/ sonar和覆盖率官方指导
https://community.sonarsource.com/c/announce/guides/22 sonar官方社区
https://blog.csdn.net/baidu_36943075/article/details/90634404 goTest使用指导
https://blog.csdn.net/baidu_36943075/article/details/90634160 sonar集成golang检测
### 6.2流程
0.首先sonar不负责执行测试,只是根据测试报告和覆盖率报告来检测,下面是如何生成扫描报告
1.先在gitlab-ci.yml文件中配置sonar,指定覆盖率和测试的文件夹,并过滤扫描*_test.go这类文件
其中scanner中的 -Dsonar. 代替 sonar.
2.在项目待测试目录执行 go test -v -json > ./report/reportTest/test.json 进行全局的测试
test.json对应上图第二个红框配置
3.在项目待测试目录目录执行 go test -v -coverprofile=./report/reportCover/covprofile 生成的 covprofile文件是覆盖率测试报告。
*.再任意目录执行测试覆盖率,要指定生成文件位置在sonar的配置位置 go test -coverprofile=/c/xx/software/golang_08/go_code/src/test_sonar/report/reportCover/profile.out
4.push后,等待更新扫描完成,到sonarqube中可以看到指定分支有代码测试覆盖率出现。
5.对于打桩不会有测试覆盖。
6.父目录下测试其子目录的所有测试覆盖率 (./...表示当前目录下所有子目录)
```shell
go test ./... - coverprofile=/c/xx/software/golang_08/go_code/src/test_sonar/report/reportCover/profile.out
7.调研测集成试增量覆盖率
tips:
//拉取项目后都要执行测试框架的安装
// gitlab上的go版本和本地不同要执行 go mod vendor
// 安装 go get github.com/smartystreets/goconvey
// go test -v -cover //-v表示当前路径所有文件,-cover表示在终端查看覆盖率
// go test -v //本地进行测试,输出结果到终端
go test -coverprofile=covprofile 对当前目录下进行测试覆盖率生成covprofile文件
go test -v -cover //-v表示显示测试详细信息,-cover表示在终端查看覆盖率
go test -v … //用三个点来表示测试整个项目
go tool cover -html=cover.out -o coverage.html //生成HTML格式的覆盖率详细信息
6.3覆盖率有效的配置文件
## 7.本地-gitlab-服务器同步进行测试流程
1.在本地将分支切换到要同步的分支(dev_lq),进行pull git pull origin dev_ 这样保证最新版本
2.在本地编写测试用例,(可能要安装引用goconvey和gostub包(go mod vendor go get -d xx)),编写完毕
包安装指令:
govendor fetch github.com/smartystreets/goconvey
govendor fetch github.com/agiledragon/gomonkey
go get -d github.com/smartystreets/goconvey
go get -d github.com/agiledragon/gomonkey
go mod vendor
go test -gcflags -v sonar1_test.go
3.更新本地仓库 git add . git commit -m "feat(qi.liang):add"
4.推送到远程同名分支 git push -u origin dev_lq
5.进入到服务器docker中
6.docker ps //显示为某种镜像创建的某个容器 的具体信息包括id
docker exec -it xxxx bash //通过bash启动id为:xxxx 的容器
7.在容器中同步拉取分支(要进到gopath的src目录下)
git clone https://gitxxx.git
(输入账号密码)
git checkout ww 从master切换到dev_lq分支
或者新建分支
git checkout -b dev__v1
同步子库
git submodule init && git submodule update
go mod vendor
8.在容器中定位到要生成覆盖率的文件夹下面
go test ./... -gcflags=-l -v -coverprofile=/xx/profile.out
上面的语句对当前文件夹下面的所有子文件夹里的_test.go进行测试并在项目根目录的-report生成覆盖率报告
具体覆盖率文件名字要和gitlab-ci.yml配置中相同。
只执行单个包的单元测试用下面的语句:
go test -v worker_test.go worker.go xx.go
上面的语句要将worker_test.go源文件worker.go和引用的相关包都要加到命令行,否则报未定义
9.生成覆盖率文件完毕,要进行git
到项目根目录
git add .
git commit -m "feat(xx):add"
git push -u origin xx//注意要本地当前分支和远程要push的分支要同名
10.提示文件不存在
git submodule init && git submodule update 项目中的子文件要用这条语句同步
11.删除git-lab远程分支指令
git push origin --delete dev_
12.远程上库 go 环境设置
GOSUMDB="off"
7.本地-gitlab-服务器同步进行测试流程
1.在本地将分支切换到要同步的分支(dev_),进行pull git pull origin dev_ 这样保证最新版本
2.在本地编写测试用例,(可能要安装引用goconvey和gostub包(go mod vendor go get -d xx)),编写完毕
包安装指令:
govendor fetch github.com/smartystreets/goconvey
govendor fetch github.com/agiledragon/gomonkey
go get -d github.com/smartystreets/goconvey
go get -d github.com/agiledragon/gomonkey
go mod vendor
go test -gcflags -v sonar1_test.go
3.更新本地仓库 git add . git commit -m “feat(qi.liang):add”
4.推送到远程同名分支 git push -u origin dev_lq
5.进入到服务器docker中
6.docker ps //显示为某种镜像创建的某个容器 的具体信息包括id
docker exec -it xxxx bash //通过bash启动id为:xxxx 的容器
7.在容器中同步拉取分支(要进到gopath的src目录下)
git clone xxx
(输入账号密码)
git checkout dev_ 从master切换到dev_分支
或者新建分支
git checkout -b dev__v1
同步子库
git submodule init && git submodule update
go mod vendor
8.在容器中定位到要生成覆盖率的文件夹下面
go test ./… -gcflags=-l -v -coverprofile=/root/go/src/xx/report/reportCover/profile.out
上面的语句对当前文件夹下面的所有子文件夹里的_test.go进行测试并在项目根目录的-report生成覆盖率报告
具体覆盖率文件名字要和gitlab-ci.yml配置中相同。
只执行单个包的单元测试用下面的语句:
go test -v worker_test.go worker.go xx.go
上面的语句要将worker_test.go源文件worker.go和引用的相关包都要加到命令行,否则报未定义
9.生成覆盖率文件完毕,要进行git
到项目根目录
git add .
git commit -m “feat():add”
git push -u origin dev_ //注意要本地当前分支和远程要push的分支要同名
10.提示文件不存在
git submodule init && git submodule update 项目中的子文件要用这条语句同步
11.删除git-lab远程分支指令
git push origin --delete dev_
12.远程上库 go 环境设置
GOSUMDB=“off”
14.docker拉取镜像并后台开启
docker pull images
docker images 显示所有镜像
docker run --name new_name -d images 返回容器ID
docker ps 显示所有后台容器
docker ps -a 显示所有包括未运行的容器
docker rm container_id 删除指定id的容器
docker stop container_id 停止指定容器
docker start container_id 开启指定容器
docker exec -it container_id bash 运行指定的容器