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

相关推荐
winks32 分钟前
Spring Task的使用
java·后端·spring
Null箘3 分钟前
从零创建一个 Django 项目
后端·python·django
秋意钟13 分钟前
Spring新版本
java·后端·spring
苏三有春26 分钟前
五分钟学会如何在GitHub上自动化部署个人博客(hugo框架 + stack主题)
git·go·github
小蜗牛慢慢爬行33 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
A小白59081 小时前
Docker部署实践:构建可扩展的AI图像/视频分析平台 (脱敏版)
后端
goTsHgo1 小时前
在 Spring Boot 的 MVC 框架中 路径匹配的实现 详解
spring boot·后端·mvc
waicsdn_haha1 小时前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
Q_19284999061 小时前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
良许Linux1 小时前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网