Byte open source golang unit testing framework mockey practice

unit test definition

Unit testing is used to verify the correctness of the code

The code being verified can be a module, a class, a function or a method

Correctness means that given input, the expected output can always be obtained

This article will analyze some problems existing in golang's native unit testing and how to use frameworks such as goconvey and mockey to solve these problems.

golang native unit testing

Quick Start:

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")
   }
}

After entering the directory where the current file is located, enter the command line: go test -run TestFunA -v. The unit test results are as follows:

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

In this way, we have completed the work of using golang native unit testing to test the logic of our code

Problems

There are two main problems with native unit testing:

  1. Lack of assertion function, output not intuitive and concise, no hierarchical relationship
  2. The mock function is not supported. If the tested code has external dependencies, it may not be able to meet the correctness requirements.

In order to solve the above two problems, we introduced goconvey and mockey

  • goconvey: comes with rich assertion functions, supports multi-level nested single tests, and outputs clear single test results.
  • mockey: supports mock function, generally used in conjunction with goconvey

goconvey

go mod

go get github.com/smartystreets/goconvey 

Quick start

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")
         })
      })
   })
}

After entering the directory where the current file is located, enter the command line: go test -run TestConveyFuncA -v. The unit test results are as follows:

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

This article will not discuss goconvey too much. Interested students can explore other uses of goconvey.

mockey

Byte's open source golang unit testing framework supports mock function and is generally used in conjunction with goconvey.

An overview of the features is as follows:

  • variable

    • Basic mock

      • Ordinary variables
      • function variable
  • function/method

    • Basic mock

      • Ordinary function
      • Ordinary method
      • Private type methods
      • Methods of anonymous struct
    • Other functions

      • goroutine conditional filtering
      • Get the number of executions of the original function
      • Get the number of mock function executions

go mod

go get github.com/bytedance/mockey@latest 

mock variable

Commonly used APIs

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

After entering the directory where the current file is located, enter the command line: go test -run TestMockVar -v. The unit test results are as follows:

=== RUN   TestMockVar

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


5 total assertions

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

PatchConvey function

Commonly used APIs

=== RUN   TestMockVar

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


5 total assertions

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

The demo has been given in the mock variable above

mock function/method

Commonly used APIs

// 开始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 function 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")
   })
}

After entering the directory where the current file is located, enter the command line: go test -run TestMockFunc -v -gcflags="all=-l -N". The unit test results are as follows:

=== 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 method 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")
      })
   })
}

After entering the directory where the current file is located, enter the command line: go test -run TestMockMethod -v -gcflags="all=-l -N". The unit test results are as follows:

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

Utility function

// 作用: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())
      })
   })
}

After entering the directory where the current file is located, enter the command line: go test -run TestGetMethod -v -gcflags="all=-l -N". The unit test results are as follows:

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

Summarize

This article focuses on sharing how to use the goconvey + mockey framework to complete our unit testing and help us verify the correctness of the code. We also welcome everyone to practice more in daily work and share more experiences.

Finally: The complete software testing video tutorial below has been compiled and uploaded. Friends who need it can get it by themselves [guaranteed 100% free]

Software Testing Interview Document

We must study to find a high-paying job. The following interview questions are the latest interview materials from first-tier Internet companies such as Alibaba, Tencent, Byte, etc., and some Byte bosses have given authoritative answers. After finishing this set I believe everyone can find a satisfactory job based on the interview information.

Guess you like

Origin blog.csdn.net/wx17343624830/article/details/133362332