Go如何做单元测试

324 阅读2分钟

testing

单元测试

单元测试 除用来测试逻辑算法是否符合预期之外,还承担着监控代码质量的责任。在任何时候都可以通过简单的命令来验证被测试函数的全部功能,,找出未完成的任务和任何因修改而造成的错误。它与性能测试、代码覆盖率等一起保证了代码总是在可控范围内。在编写测试文件时,需要注意以下几点

  1. 导入 testing

  2. 将测试文件命名:被测试文件名_test.go

  3. 测试文件一般推荐写在被测试函数的统计目录下,并且以 Test_被测试函数名(t *testing.T) 进行命名

  4. 测试命令 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)
   }
}
  1. gomock.NewController :Controller表示模拟生态系统的顶级控制。它定义模拟对象的作用域、生命周期,以及它们的期望。从多个goroutine调用Controller的方法是线程安全的。每个测试都应该创建一个新的Controller。
  2. mock_dal.NewMockPerson :创建一个模拟(mock)的Person对象。
  3. 方法介绍:
    • EXPECT() :返回一个对象,该对象允许调用者设置期望的返回值
    • Eat() :设置入参并调用mock对象中的方法
      • gomock.Any() 表示匹配任意类型入参。
      • gomock.Eq(x) : 使用反射来匹配与x深度相等的值。
      • gomock.Nil() : 匹配nil
      • gomock.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

mocky