testing
单元测试
单元测试 除用来测试逻辑算法是否符合预期之外,还承担着监控代码质量的责任。在任何时候都可以通过简单的命令来验证被测试函数的全部功能,,找出未完成的任务和任何因修改而造成的错误。它与性能测试、代码覆盖率等一起保证了代码总是在可控范围内。在编写测试文件时,需要注意以下几点
-
导入
testing包 -
将测试文件命名:
被测试文件名_test.go -
测试文件一般推荐写在被测试函数的统计目录下,并且以
Test_被测试函数名(t *testing.T)进行命名 -
测试命令
go test会忽略以_和.开头都测试文件,同时正常的编译操作会忽略测试文件
目录结构及代码示例
example
├──add.go
└──add_test.go
add.go
package example
func Add(i, j int) int {
return i + j
}
add_test.go
package example
import (
"testing"
)
func TestAdd(t *testing.T) {
var ts = []struct {
i int
j int
expect int
}{
{1, 2, 3}, {2, 3, 5}, {3, 5, 8},
}
for _, s := range ts {
if !assert.Equal(t, s.expect, s.i+s.j) {
t.Errorf("1 add 2 shoud be 3, but res is %d", s.i+s.j)
}
}
}
在 teminal 中运行 go test 执行当前 pacakge 下的所有测试用例
go test ./pkg/test -v
参数说明
-
-args命令行参数 -
-cover查看覆盖率 -
-run regexp只运行 regexp 匹配的函数,例如:-run Array 那么就执行包含有 Array 开头的函数,该参数支持通配符*,和部分正则表达式,例如^、$ -
-v显示测试的详细信息 -
-count重复测试次数,默认为1 -
-timeout全部测试时间累积超过会引发panic, 默认值为10ms
执行结果
$ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok example 0.005s
控制测试结果和行为
t.Fail //失败, 继续执行当前测试函数
t.FailNow //失败,停止执行当前测试函数
t.SkipNow //跳过执行当前测试函数
t.Log //输出错误信息,仅当失败或者 -v 时
t.Error // Fail + Log
t.Fatal // FailNow + Log
性能测试
性能测试函数同样放在 *_test.go 文件里,并以 Benchmark 为名称前缀
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++{
_ =Add(1, 2)
}
}
测试工具默认不会执行性能测试,必须只用 bench 参数, 它通过逐步调整 B.N 的值,反复执行测试函数,直到得到准确的结果
$ go test -bench .
goos: darwin
goarch: amd64
pkg: example
cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
BenchmarkAdd-4 1000000000 0.2822 ns/op
PASS
ok demo/testttt 0.320s
模糊测试
下面介绍一些常用的开源测试框
mock
为什么要使用mock
在对有外部资源或依赖关系的代码进行单元测试比较麻烦。
gomock 是一个模拟框架,它很好的集成了Go语言内置的 testing 包,在进行单元测试时提供了灵活性,同时也能在其他的上下文中使用。
安装
安装gomcok包: go get github.com/golang/mock/gomock go get
安装代码生成工具: mockegen``github.com/golang/mock/mockgen
项目结构
example
├── person
│ └── person.go
│ └── mocks
│ └── mock_person.go
└── student
├── student.go
└── student_test.go
example/person/person.go
package person
//go:generate mockgen -destination=./mocks/mock_person.go -package=mocks example/person Person
type Person interface {
Eat(int) string
SayHello(string) string
}
example/student/student.go
package student
import (
"example/person"
"fmt"
)
type Student struct {
Name string
P person.Person
}
var _ person.Person = (*Student)(nil)
func (s *Student) Eat(w int) string {
if w > 10 {
return "There are too many food..."
}
return "I am full"
}
func (s *Student) SayHello(name string) string {
return fmt.Sprintf("Hello %s, I am %s", name, s.Name)
}
func (s *Student) Calculate(i, j int) int {
return i + j
}
通过 go generate 生成 mock 文件
go:generate mockgen -destination=./mocks/mock_person.go -package=mocks example/person Person
- -destination:指定生成的mock文件路径以及名。
- -packge:设置mock文件的包名。若不设置,则为
mock_前缀加文件名。 - example/person 指定需要生成mock文件的包
- Person 指定需要生成mock文件的接口
测试用例
func TestStudent_Eat(t *testing.T) {
controller := gomock.NewController(t)
mockPerson := mocks.NewMockPerson(controller)
mockStu := &Student{Name: "zhang3", P: mockPerson}
_ = mockPerson.EXPECT().Eat(gomock.Any()).Return("I am full").AnyTimes()
for i := 0; i < 10; i++ {
mockStu.Eat(i)
}
}
gomock.NewController:Controller表示模拟生态系统的顶级控制。它定义模拟对象的作用域、生命周期,以及它们的期望。从多个goroutine调用Controller的方法是线程安全的。每个测试都应该创建一个新的Controller。mock_dal.NewMockPerson:创建一个模拟(mock)的Person对象。- 方法介绍:
EXPECT():返回一个对象,该对象允许调用者设置期望的返回值Eat():设置入参并调用mock对象中的方法gomock.Any()表示匹配任意类型入参。gomock.Eq(x): 使用反射来匹配与x深度相等的值。gomock.Nil(): 匹配nilgomock.Not(m): 这里的m是一个Matcher,匹配同m不匹配的值
Return():设置返回值AnyTimes():控制调用次数,也可以用Times()进行限制
返回根目录执行 go test ./student -run=TestStudent_Eat$ -v
=== RUN TestStudent_Eat
--- PASS: TestStudent_Eat (0.00s)
PASS
ok example/student 0.433s
用 gintest 测试api
项目结构
example
├── server
│ ├── demo.go
│ └── login_test.go
└── main.go
demo.go
package server
import (
"github.com/gin-gonic/gin"
)
type UserLoginReq struct {
Name string `json:"name"`
Password string `json:"password"`
}
type UserLoginResp struct {
Msg string `json:"msg"`
}
func Server() {
r := gin.Default()
user := r.Group("/user", middleAuth())
user.POST("/login", handleLogin)
r.Run()
}
func handleLogin(c *gin.Context) {
req := new(UserLoginReq)
if err := c.ShouldBind(req); err != nil {
return
}
resp := new(UserLoginResp)
if req.Name == "" || req.Password == "" {
resp.Msg = "登录失败"
c.JSON(200, resp)
return
}
resp.Msg = "登陆成功"
c.JSON(200, resp)
}
func middleAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if false {
c.Abort()
}
c.Next()
}
}
package server
import (
"encoding/json"
"testing"
"github.com/gotomicro/unittest/gintest"
"github.com/stretchr/testify/assert"
)
func TestLogin(t *testing.T) {
testObj := gintest.Init()
testObj.POST(handleLogin, func(m *gintest.Mock) error {
byteInfo := m.Exec(
gintest.WithUri("/user/login"),
gintest.WithJsonBody(UserLoginReq{
Name: "zhang3",
Password: "123456",
}))
var resp UserLoginResp
err := json.Unmarshal(byteInfo, &resp)
assert.NoError(t, err)
assert.Equal(t, "登陆成功", resp.Msg)
return nil
},gintest.WithRoutePath("/user/login"),
gintest.WithRouteMiddleware(middleAuth()))
_ = testObj.Run()
}
测试结果
=== RUN TestLogin
--- PASS: TestLogin (0.00s)
PASS
Process finished with the exit code 0