正常来说,写了go代码跑起来,是将整个工程跑起来,比如一个goframe项目那么直接将g.server跑起来,也就是从入口文件
然后通过postman访问路由验证
但是很多时候有一些简单的静态方法像java可以在spring的项目之中任意文件起一个main函数调用验证static的一些方法
那么go的话,难道测试单一模块需要到处写main函数吗?还是非得将整个工程跑起来吗?
试用一下golang test
主要功能点:
1、不用到处写main函数调用就可以测试单一方法;
2、可以使用断言方法方便多次验证接口通过率;
3、看起来高大上,如果在项目之中到处写用于test的main函数不是很low么,一不小心提交到git上了;
4、写好测试逻辑后可以很方便的多次、高频测试;
5、方便压测统计
使用方式:
1 编写测试代码*_test.go , 注意:文件名必须是 _test.go结尾
package c import ( "testing" ) // 验证gconv struct会不会抛出空指针异常 func Test_Trim(t *testing.T) { t.Log("start test", t.Name()) c := Add(1, 2) if c != 3 { // 判定不会爆空指针异常 t.Log("Add function error") t.FailNow() } t.Log("very good") } func Add(a int, b int) int { return a + b + 1 }
2 测试单个文件,一定要带上被测试的原文件(所有有依赖的文件名都要带上,所以这个一般不会使用,不会测试单个文件,只限制单个方法就好了)
go test -v wechat_test.go wechat.go
3 测试单个方法
go test -v wechat_test.go -test.run TestRefreshAccessToken //如果不指定文件名,会将当前目录的所有test方法进行测试 、 如果不指定方法名将会验证所有方法
最常用的用法当然是
go test -test.run TestGetProductName
测试单个方法,这样也不用指定有依赖的文件列表
4 压力测试
package c import ( "testing" "github.com/stretchr/testify/assert" ) // 验证gconv struct会不会抛出空指针异常 func TestGetProductName(t *testing.T) { productID := 3 t.Log("start TestGetUserName", productID) c := GetProductName(1) assert.Equal(t, "iphone 12", c, "商品名称必须是 iphone 12") } // GetProductName 获取商品名称 func GetProductName(productID int) string { // 从redis获取商品数据 // 如果redis没有就查询数据库 return "iphone 12" } func TestGetUserName(t *testing.T) { userID := 2 t.Log("start TestGetUserName", userID) c := GetUserName(userID) assert.Equal(t, "jack", c, "商品名称必须是 jack") } // GetUserName 获取用户名称 func GetUserName(userID int) string { // 从redis获取用户名称 // 如果redis没有就查询数据库 return "jack" } // 1 基准测试的代码文件必须以_test.go结尾 // 2 基准测试的函数必须以Benchmark开头,必须是可导出的 // 3 基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数 // 4 基准测试函数不能有返回值 // 5 b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰 // 6 最后的for循环很重要,被测试的代码要放到循环里 // 7 b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能 // BenchmarkGetProductName 压力测试获取商品名称接口 func BenchmarkGetProductName(b *testing.B) { num := 10 // 之所以增加这个函数是之前的初始化时间不算在压测的这个接口的执行时间内 b.ResetTimer() for i := 0; i < b.N; i++ { GetUserName(num) // if i == 12 { // b.Error() // } } } // BenchmarkGetUserName 压力测试获取商品名称接口 func BenchmarkGetUserName(b *testing.B) { num := 10 // 之所以增加这个函数是之前的初始化时间不算在压测的这个接口的执行时间内 b.ResetTimer() for i := 0; i < b.N; i++ { GetUserName(num) } }
单元测试获取商品名称接口:
// 指定仅仅运行 product_test 文件的 TestGetProductName 函数 go test -v product_test.go -test.run TestGetProductName
如何对获取商品名称接口进行压测呢?
推荐阅读:https://cloud.tencent.com/developer/article/1469185
验证代码:
// 1 基准测试的代码文件必须以_test.go结尾 // 2 基准测试的函数必须以Benchmark开头,必须是可导出的 // 3 基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数 // 4 基准测试函数不能有返回值 // 5 b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰 // 6 最后的for循环很重要,被测试的代码要放到循环里 // 7 b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能 // BenchmarkGetProductName 压力测试获取商品名称接口 func BenchmarkGetProductName(b *testing.B) {
b.N = 1000000 // 修改测试次数是100w次 num := 10 // 之所以增加这个函数是之前的初始化时间不算在压测的这个接口的执行时间内 b.ResetTimer() for i := 0; i < b.N; i++ { GetProductName(num) } } // BenchmarkGetUserName 压力测试获取商品名称接口 func BenchmarkGetUserName(b *testing.B) { num := 10 // 之所以增加这个函数是之前的初始化时间不算在压测的这个接口的执行时间内 b.ResetTimer() for i := 0; i < b.N; i++ { GetUserName(num) } }
现在对获取商品接口压测 2000次并发调用:
go test -bench=. -run=none
- bench 表示对哪些函数左压测 ,如果只对 BenchmarkGetUserName 压测则 -bench=BenchmarkGetUserName
输出:
$ go test -bench=. -run=none goos: windows goarch: amd64 pkg: example.com/m/v cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkGetProductName-4 1000000000 0.2947 ns/op BenchmarkGetUserName-4 1000000000 0.2953 ns/op PASS ok example.com/m/v 3.688s
函数执行次数 1000000000, 各自平均执行时间是 0.2947 纳秒和 0.2953纳秒
总共执行时长是 3.688s
还可以使用 pprof 查看内存和CPU占用
现在我们可以知道我们的接口执行完成需要花费的时间,但是我们并不知道调用接口的时候有没有出现请求失败的情况,
比如在一万个请求过来的时候,它的执行,性能瓶颈是在MySQL还是Redis,会不会变得等待很久,就是一个临界点,10qps的时候每个接口执行1s,那1w个qps的话执行时长是多少?
如何验证?
感觉基准测试根本不满足,宁愿自己手写脚本验证,并且请求过去
目前更多的就是简单的拿到一段代码执行的CPU、内存消耗、执行时长,但是并不能用来做并发的压测
一个连接数据库的小脚本、可以用于测试:
package test import ( "database/sql" _ "github.com/go-sql-driver/mysql" ) var ( dbhostsip = "127.0.0.1:3306" dbusername = "user" dbpassowrd = "123" dbname = "test" ) type mysql_db struct { db *sql.DB //定义结构体 } func (f *mysql_db) mysql_open() { //打开 Odb, err := sql.Open("mysql", dbusername+":"+dbpassowrd+"@tcp("+dbhostsip+")/"+dbname) if err != nil { panic(err) } f.db = Odb } func (f *mysql_db) mysql_close() { //关闭 defer f.db.Close() } func (f *mysql_db) mysql_select(sql_data string) string { rows, err := f.db.Query(sql_data) if err != nil { println(err) } user_id := "" for rows.Next() { var id int var name string err = rows.Scan(&id, &name) if err != nil { panic(err) } user_id = name } return user_id } // type Log struct { // ID string `json:"id"` // Name string `json:"id"` // } func GetDB() string { db := &mysql_db{} db.mysql_open() u := db.mysql_select("SELECT * FROM user") db.mysql_close() //关闭 return u }
优秀的写法示例:
go-zero框架的单元测试示例
参考文章:
https://goframe.org/pages/viewpage.action?pageId=1114153
https://blog.csdn.net/nimei31/article/details/81385207
http://c.biancheng.net/view/124.html
单元测试工具包:
https://github.com/stretchr/testify