字节开源golang单元测试框架mockey实践

单元测试的定义

单元测试是用来验证代码的正确性

被验证的代码可以是一个模块,一个类,一个函数或者方法

正确性是指在给定的输入下,总能得到预期的输出

本文会分析golang原生单元测试存在的一些问题,以及如何使用goconvey和mockey等框架解决这些问题

golang原生单元测试

快速入门:

go 复制代码
package test

import (
   "strings"
   "testing"
)

func funcA(s string) string {
   return s
}

// 执行整个包的单元测试 go test -v 其中-v表示打印测试函数的所有细节
// 指定运行某一测试函数 go test -run TestFunA -v
func TestFunA(t *testing.T) {
   if !strings.EqualFold(funcA("aa"), "aaa") {
      t.Errorf("Test Case1 fail")
   }
   if !strings.EqualFold(funcA("bbb"), "bbb") {
      t.Errorf("Test Case2.1 bbb")
   }
   if !strings.EqualFold(funcA("cc"), "ccc") {
      t.Errorf("Test Case2.2 hello")
   }
}

进入到当前文件所在目录后,命令行输入:go test -run TestFunA -v,单元测试结果如下:

lua 复制代码
=== RUN   TestFunA
    native_test.go:22: Test Case1 fail
    native_test.go:28: Test Case2.2 hello
--- FAIL: TestFunA (0.00s)
FAIL
exit status 1
FAIL   xxx/framework/all/test2    1.417s

这样我们就完成了使用golang原生单元测试来测试我们代码逻辑的工作

存在的问题

原生单元测试主要存在以下两个大的问题:

  1. 缺少断言函数,输出不够直观简洁,没有层级关系
  2. 不支持mock功能,如果测试的代码对外部有依赖,可能没办法满足正确性

为了解决以上两个问题,我们引入了goconvey和mockey

  • goconvey:自带丰富的断言函数,支持多层级嵌套单测,输出清晰的单测结果
  • mockey:支持mock功能,一般结合goconvey一起使用

goconvey

go mod

arduino 复制代码
go get github.com/smartystreets/goconvey

快速入门

go 复制代码
package test

import (
   . "github.com/smartystreets/goconvey/convey"
   "testing"
)

func funcA(s string) string {
   return s
}

// go test -run TestConveyFuncA -v
func TestConveyFuncA(t *testing.T) {
   // 最外层Convey函数签名:Convey(description string, t *testing.T, action func())
   Convey("TestConveyHello", t, func() {
      // 不是最外层的Convey,不需要参数t,函数签名:Convey(description string, action func())
      Convey("Test Case1", func() {
         // 支持丰富的断言
         So(funcA("aa"), ShouldEqual, "aaa")
      })

      Convey("Test Case2", func() {
         // Convey可以无限嵌套
         Convey("Test Case2.1", func() {
            So(funcA("bbb"), ShouldEqual, "bbb")
         })
         Convey("Test Case2.2", func() {
            So(funcA("cc"), ShouldEqual, "hello")
         })
      })
   })
}

进入到当前文件所在目录后,命令行输入:go test -run TestConveyFuncA -v,单元测试结果如下:

bash 复制代码
=== RUN   TestConveyFuncA

  TestConveyHello 
    Test Case1 ✘
    Test Case2 
      Test Case2.1 ✔
      Test Case2.2 ✘


Failures:

  * xxx/framework/all/test2/convey_test.go 
  Line 18:
  Expected: "aaa"
  Actual:   "aa"
  (Should equal)!
  Diff:     '"aaa"'

  * xxx/framework/all/test2/convey_test.go 
  Line 27:
  Expected: "hello"
  Actual:   "cc"
  (Should equal)!


3 total assertions

--- FAIL: TestConveyFuncA (0.00s)
FAIL
exit status 1
FAIL    xxx/framework/all/test2    1.425s

本文不对goconvey做过多的展开讨论,感兴趣的同学可以多探索下goconvey的其他用法

mockey

字节开源的golang单元测试框架,支持mock功能,一般结合goconvey一起使用

功能概览如下:

  • 变量

    • 基础mock

      • 普通变量
      • 函数变量
  • 函数/方法

    • 基础mock

      • 普通函数
      • 普通方法
      • 私有类型的方法
      • 匿名struct的方法
    • 其他功能

      • goroutine 条件过滤
      • 获取原函数执行次数
      • 获取mock函数执行次数

go mod

kotlin 复制代码
go get github.com/bytedance/mockey@latest

mock变量

常用api

scss 复制代码
// 开始mock
// targetPtr:需要mock的变量地址
func MockValue(targetPtr interface{}) *MockerVar
// 设置变量
// value:mock的新值
func (mocker *MockerVar) To(value interface{}) *MockerVar

// 手动取消mock
func (mocker *MockerVar) UnPatch() *MockerVar
// 手动再次mock
func (mocker *MockerVar) Patch() *MockerVar

demo

scss 复制代码
package test

import (
   . "github.com/bytedance/mockey"
   . "github.com/smartystreets/goconvey/convey"
   "testing"
)

var (
   one = "one"
)

// go test -run TestMockVar -v
func TestMockVar(t *testing.T) {
   PatchConvey("mock变量", t, func() {
      // PatchConvey执行结束后,自动释放内部的patch,免去defer的苦恼
      PatchConvey("mock普通变量", func() {
         MockValue(&one).To("one v2")
         So(one, ShouldEqual, "one v2")
      })

      a := 10
      PatchConvey("mock函数变量", func() {
         MockValue(&a).To(20)
         So(a, ShouldEqual, 20)
      })

      PatchConvey("手动取消mock,手动再次mock", func() {
         mockB := MockValue(&a).To(20)
         So(a, ShouldEqual, 20)
         
         // 手动取消mock
         mockB.UnPatch()
         So(a, ShouldEqual, 10)
         
         // 手动再次mock
         mockB.Patch()
         So(a, ShouldEqual, 20)
      })
   })
}

进入到当前文件所在目录后,命令行输入:go test -run TestMockVar -v,单元测试结果如下:

diff 复制代码
=== RUN   TestMockVar

  mock变量 
    mock普通变量 ✔
    mock函数变量 ✔
    手动取消mock,手动再次mock ✔✔✔


5 total assertions

--- PASS: TestMockVar (0.00s)
PASS
ok      xxx/framework/all/test2    1.349s

PatchConvey函数

常用api

go 复制代码
// 替代goconvey的Convey函数,用法与Convey完全一致
// 不同点:自动释放当前convey内部的patch,免去defer的苦恼
func PatchConvey(items ...interface{})

demo已经在上面mock变量中给出

mock函数/方法

常用api

scss 复制代码
// 开始mock
// target:需要mock的函数/方法
func Mock(target interface{}, opt ...optionFn) *MockBuilder

// mock方式一:直接设置结果
// results参数列表需要完全等同于需要mock的函数返回值列表
func (builder *MockBuilder) Return(results ...interface{}) *MockBuilder
// mock方式二:使用mock函数
// hook 参数与返回值需要与mock函数完全一致,注意类成员函数需要增加self作为第一个参数(目前已经兼容了不传入receiver,当不需要使用的时候可以忽略)
func (builder *MockBuilder) To(hook interface{}) *MockBuilder

// 可选项
// 条件设置
// when:表示在何种条件下调用mock函数返回mock结果
// 函数原型:when(args...) bool
// args:与Mock 函数参数一致,一般通过args来判断是否需要执行 mock,注意类成员函数需要增加self作为第一个参数(目前已经兼容了不传入receiver,当不需要使用的时候可以忽略)
// 返回值:bool。是true的时候执行mock
func (builder *MockBuilder) When(when interface{}) *MockBuilder
// mock访问goroutine限制
// 只在当前goroutine执行mock
func (builder *MockBuilder) IncludeCurrentGoRoutine() *MockBuilder
// 不再当前goroutine执行mock
func (builder *MockBuilder) ExcludeCurrentGoRoutine() *MockBuilder
// 过滤指定的goroutine
// filter:过滤类型Disable = 0不启用mock,Include = 1,Exclude = 2
// gId:指定的goroutine,可通过工具函数获取,工具函数下面会介绍
func (builder *MockBuilder) FilterGoRoutine(filter FilterGoroutineType, gId int64) *MockBuilder

// 创建mock
func (builder *MockBuilder) Build() *Mocker

// 手动取消mock代理
func (mocker *Mocker) UnPatch() *Mocker
// 手动启用mock代理
func (mocker *Mocker) Patch() *Mocker

// mock的统计结果,一般用于结果断言。注意每次重新mock或修改mock都会重置为0
// 被mock函数调用的次数
func (mocker *Mocker) Times() int
// hook函数调用的次数
func (mocker *Mocker) MockTimes() int

mock函数demo:

go 复制代码
package test

import (
   . "github.com/bytedance/mockey"
   . "github.com/smartystreets/goconvey/convey"
   "testing"
)

func funcA(s string) string {
   return s
}

// go test -run TestMockFunc -v -gcflags="all=-l -N"
// 使用-gcflags="all=-l -N",禁用内联和编译优化
func TestMockFunc(t *testing.T) {
   PatchConvey("mock函数方式1", t, func() {
      Mock(funcA).Return("mock s").Build()
      So(funcA("hello"), ShouldEqual, "mock s")
   })

   PatchConvey("mock函数方式2", t, func() {
      Mock(funcA).To(func(s string) string {
         return "mock s"
      }).Build()
      So(funcA("hello"), ShouldEqual, "mock s")
   })

   PatchConvey("mock函数,使用when来决定是否需要mock", t, func() {
      Mock(funcA).When(func(s string) bool {
         return s == "hello1"
      }).To(func(s string) string {
         return "mock s"
      }).Build()
      So(funcA("hello1"), ShouldEqual, "mock s")
      So(funcA("hello"), ShouldEqual, "mock s")
   })

   PatchConvey("mock函数,手动取消mock,手动再次mock", t, func() {
      m := Mock(funcA).To(func(s string) string {
         return "mock s"
      }).Build()
      m.IncludeCurrentGoRoutine()
      So(funcA("hello"), ShouldEqual, "mock s")
      So(m.Times(), ShouldEqual, 1)
      So(m.MockTimes(), ShouldEqual, 1)

      // 手动取消
      m.UnPatch()
      So(funcA("hello"), ShouldEqual, "hello")

      // 手动再次mock
      m.Patch()
      So(funcA("hello"), ShouldEqual, "mock s")
   })
}

进入到当前文件所在目录后,命令行输入:go test -run TestMockFunc -v -gcflags="all=-l -N",单元测试结果如下:

less 复制代码
=== RUN   TestMockFunc

  mock函数方式1 ✔


1 total assertion


  mock函数方式2 ✔


2 total assertions


  mock函数,使用when来决定是否需要mock ✔✘


Failures:

  * xxx/framework/all/test2/mockey_test.go 
  Line 78:
  Expected: "mock s"
  Actual:   "hello"
  (Should equal)!


4 total assertions


  mock函数,手动取消mock,手动再次mock ✔✔✔✔✔


9 total assertions

--- FAIL: TestMockFunc (0.00s)
FAIL
exit status 1
FAIL    xxx/framework/all/test2    2.087s

mock方法demo

go 复制代码
package test

import (
   . "github.com/bytedance/mockey"
   . "github.com/smartystreets/goconvey/convey"
   "testing"
)

type Class struct {
}

func (Class) FunA(s string) string {
   return s
}

func (*Class) FunB(s string) string {
   return s
}

// go test -run TestMockMethod -v -gcflags="all=-l -N"
func TestMockMethod(t *testing.T) {
   PatchConvey("mock方法方式1", t, func() {
      PatchConvey("mock方法方式1.1 - 非指针", func() {
         Mock(Class.FunA).Return("mock s").Build()
         So(Class{}.FunA("hello"), ShouldEqual, "mock s")
      })
      PatchConvey("mock方法方式1.2 - 指针", func() {
         Mock((*Class).FunB).Return("mock s").Build()
         So((&Class{}).FunB("hello"), ShouldEqual, "mock s")
      })
   })

   PatchConvey("mock方法方式2", t, func() {
      PatchConvey("mock方法方式2.1 - 非指针", func() {
         Mock(Class.FunA).To(func(self Class, s string) string {
            return "mock s"
         }).Build()
         So(Class{}.FunA("hello"), ShouldEqual, "mock s")
      })
      PatchConvey("mock方法方式2.2 - 指针", func() {
         Mock((*Class).FunB).To(func(self *Class, s string) string {
            return "mock s"
         }).Build()
         So((&Class{}).FunB("hello"), ShouldEqual, "mock s")
      })
   })
}

进入到当前文件所在目录后,命令行输入:go test -run TestMockMethod -v -gcflags="all=-l -N",单元测试结果如下:

markdown 复制代码
=== RUN   TestMockMethod

  mock方法方式1 
    mock方法方式1.1 - 非指针 ✔
    mock方法方式1.2 - 指针 ✔


2 total assertions


  mock方法方式2 
    mock方法方式2.1 - 非指针 ✔
    mock方法方式2.2 - 指针 ✔


4 total assertions

--- PASS: TestMockMethod (0.00s)
PASS
ok      xxx/framework/all/test2    1.400s

工具函数

go 复制代码
// 作用:mock私有类型的方法 或 mock匿名struct的方法,获取不到会panic
// 参数:
// instance:私有struct实例 或 含有多层嵌套匿名struct的struct实例
// methodName:对应方法名,必须是public方法
func GetMethod(instance interface{}, methodName string) interface{}
// 获取当前goroutine id,已过时,不推荐使用
func GetGoroutineId() int64

demo:

go 复制代码
package test

import (
   "fmt"
   . "github.com/bytedance/mockey"
   . "github.com/smartystreets/goconvey/convey"
   "testing"
)

type IReader interface {
   Get(key string) string
}

type reader struct {
   *Client1
}

func (r *reader) Get(s string) string {
   return r.Client1.GetKey(s)
}

func NewReader(c *Client1) IReader {
   return &reader{
      Client1: c,
   }
}

type Client1 struct {
   client2
}

type client2 struct {
}

func (c *client2) GetKey(key string) string {
   return key
}

// go test -run TestGetMethod -v -gcflags="all=-l -N"
func TestGetMethod(t *testing.T) {
   PatchConvey("工具类", t, func() {
      PatchConvey("使用GetMethod mock私有类型的方法", func() {
         r := NewReader(nil)
         Mock(GetMethod(r, "Get")).To(func(s string) string {
            return "aaa"
         }).Build()
         fmt.Println(r.Get(""))
      })

      PatchConvey("使用GetMethod mock匿名struct的方法", func() {
         r := NewReader(&Client1{})
         Mock(GetMethod(r, "GetKey")).To(func(s string) string {
            return "bbb"
         }).Build()
         fmt.Println(r.Get(""))
      })

      PatchConvey("GetGoroutineId获取当前goroutine id", func() {
         fmt.Println(GetGoroutineId())
      })
   })
}

进入到当前文件所在目录后,命令行输入:go test -run TestGetMethod -v -gcflags="all=-l -N",单元测试结果如下:

markdown 复制代码
=== RUN   TestGetMethod

  工具类 
    使用GetMethod mock私有类型的成员函数 aaa

    使用GetMethod mock匿名struct的成员函数 bbb

    GetGoroutineId获取当前goroutine id 6



0 total assertions

--- PASS: TestGetMethod (0.00s)
PASS
ok      xxx/framework/all/test2    0.613s

总结

本文重点分享了如何使用goconvey + mockey框架来完成我们的单元测试,帮助我们验证代码的正确性。也欢迎大家在日常工作中多多实践,多分享交流经验

相关推荐
PPPPickup11 小时前
easychat---创建,获取,获取详细,退群,解散,添加与移除群组
java·开发语言·后端·maven
回家路上绕了弯12 小时前
大表优化实战指南:从千万到亿级数据的性能蜕变
分布式·后端
Home12 小时前
23 种设计模式--桥接(Bridge)模式(结构型模式二)
java·后端
编程修仙12 小时前
第九篇 Spring中的代理思想
java·后端·spring
aiopencode12 小时前
iOS CPU 使用率深度分析,多工具协同定位高占用瓶颈的工程化方法
后端
I'm Jie12 小时前
告别重复编码!SpringBoot 字段变更(新旧值)日志工具类的规范化设计与优雅实现
java·spring boot·后端
开心猴爷13 小时前
Bundle Id 创建与管理的工程化方法,一次团队多项目协作中的流程重构
后端
databook13 小时前
用样本猜总体的秘密武器,4大抽样分布总结
后端·python·数据分析
小坏讲微服务13 小时前
SpringBoot4.0整合Scala完整使用
java·开发语言·spring boot·后端·scala·mybatis
泉城老铁13 小时前
windows服务器mysql数据库备份脚本
java·后端·mysql