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

2023年 9月 27日 46.2k 0

单元测试的定义

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

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

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

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

golang原生单元测试

快速入门:

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,单元测试结果如下:

=== 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原生单元测试来测试我们代码逻辑的工作

存在的问题

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

  • 缺少断言函数,输出不够直观简洁,没有层级关系
  • 不支持mock功能,如果测试的代码对外部有依赖,可能没办法满足正确性
  • 为了解决以上两个问题,我们引入了goconvey和mockey

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

    goconvey

    go mod

    go get github.com/smartystreets/goconvey
    

    快速入门

    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,单元测试结果如下:

    === 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

    go get github.com/bytedance/mockey@latest
    

    mock变量

    常用api

    // 开始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

    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,单元测试结果如下:

    === RUN   TestMockVar
    
      mock变量 
        mock普通变量 ✔
        mock函数变量 ✔
        手动取消mock,手动再次mock ✔✔✔
    
    
    5 total assertions
    
    --- PASS: TestMockVar (0.00s)
    PASS
    ok      xxx/framework/all/test2    1.349s
    

    PatchConvey函数

    常用api

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

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

    mock函数/方法

    常用api

    // 开始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:

    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",单元测试结果如下:

    === 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

    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",单元测试结果如下:

    === 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
    

    工具函数

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

    demo:

    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",单元测试结果如下:

    === 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框架来完成我们的单元测试,帮助我们验证代码的正确性。也欢迎大家在日常工作中多多实践,多分享交流经验

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论