Learning and Communication: Go Language Technology WeChat Group

Business cooperation plus WeChat: LetsFeng

Learning and sharing: GoLand2022 Genuine Activation Code for the Family Bucket Universal Edition



Textbooks, documents to learn Go language, I strongly recommend this book


Start your Go language learning journey now! Life is too short, let's Go.

picture

picture

When there is a dependency between projects, we can often test by mocking the implementation of an interface in a relatively concise and independent way.

However, in the process of mocking, because everyone's style is not uniform, and many use minimal implement for mocking, this leads to the fact that the return value of each function implemented by mock is often static, and the caller cannot be made according to the Some complex logic for the return value.

Let's take an example first

package task

type Task interface {
 Do(int) (string, error)
}
Manual mocking by minimal implement
package mock

type MinimalTask struct {
 // filed
}

func NewMinimalTask() *MinimalTask {
 return &MinimalTask{}
}

func (mt *MinimalTask) Do(idx int) (string, error) {
 return ""nil
}

In the process of other packages using Mock's implementation, it will bring some problems to the test.

For example, if we have the following interface definition and function definition

package pool

import "github.com/ultramesh/mock-example/task"

type TaskPool interface {
 Run(times int) error
}

type NewTask func() task.Task

We encapsulate an implementation based on the interface definition and interface constructor definition
package pool

import (
 "fmt"
 "github.com/pkg/errors"
 "github.com/ultramesh/mock-example/task"
)

type TaskPoolImpl struct {
 pool []task.Task
}

func NewTaskPoolImpl(newTask NewTask, size int) *TaskPoolImpl {
 tp := &TaskPoolImpl{
  pool: make([]task.Task, size),
 }
 for i := 0; i < size; i++ {
  tp.pool[i] = newTask()
 }
 return tp
}

func (tp *TaskPoolImpl) Run(times int) error {
 poolLen := len(tp.pool)
 for i := 0; i < times; i++ {
  ret, err := tp.pool[i%poolLen].Do(i)
  if err != nil {
   // process error
   return errors.Wrap(err, fmt.Sprintf("error while run task %d", i%poolLen))
  }
  switch ret {
  case "":
   // process 0
   fmt.Println(ret)
  case "a":
   // process 1
   fmt.Println(ret)
  case "b":
   // process 2
   fmt.Println(ret)
  case "c":
   // process 3
   fmt.Println(ret)
  }
 }
 return nil
}

Next, if we write the test, it should be as follows
package pool

import (
 "github.com/golang/mock/gomock"
 "github.com/stretchr/testify/assert"
 "github.com/ultramesh/mock-example/mock"
 "github.com/ultramesh/mock-example/task"
 "testing"
)

type TestSuit struct {
 name string
 newTask NewTask
 size int
 times int
}

func TestTaskPoolRunImpl(t *testing.T) {

 testSuits := []TestSuit{
  {
   nam
  e: "minimal task pool",
   newTask: func() task.Task { return mock.NewMinimalTask() },
   size: 100,
   times: 200,
  },
 }

 for _, suit := range testSuits {
  t.Run(suit.name, func(t *testing.T) {
   var taskPool TaskPool = NewTaskPoolImpl(suit.newTask, suit.size)
   err := taskPool.Run(suit.size)
   assert.NoError(t, err)
  })
 }
}

In this way, through the coverage test that comes with go test, we can see that the actual tested path of TaskPoolImpl is

picture

The problem with manual implementation of MinimalTask ​​that can be seen is that since the return value of callee is uncontrollable for caller, we can only cover the path to the return value determined by MinimalTask.

In addition, mocks are often operated by dependent projects in our practice. He does not know how the caller handles the return value. There is no way to encapsulate a simple and sufficient minimal implementation for interface testing. Therefore, we need to improve our mock. Strategy, use golang's official mock tool - gomock for better interface testing.

gomock practice

The advantage of using golang's official mock tool is that

  • Based on the mock code generated by the tool, we can encapsulate a minimal implement in a more streamlined way to achieve the same effect as manually implementing a minimal implement.

  • It allows the caller to flexibly and selectively control the input and output parameters of the interface methods that it needs to use.

Still in the example of TaskPool above, we now use the tools provided by gomock to automatically generate a mock Task

mockgen -destination mock/mock_task.go -package mock -source task/interface.go

Generate a mock_task.go in the mock package to implement the interface Task

First based on mock_task.go, we can implement a MockMinimalTask ​​for the simplest test

package mock

import "github.com/golang/mock/gomock"

func NewMockMinimalTask(ctrl *gomock.Controller) *MockTask {
 mock := NewMockTask(ctrl)
 mock.EXPECT().Do().Return(""nil).AnyTimes()
 return mock
}

So we can implement a MockMinimalTask ​​to do some tests
package pool

import (
 "github.com/golang/mock/gomock"
 "github.com/stretchr/testify/assert"
 "github.com/ultramesh/mock-example/mock"
 "github.com/ultramesh/mock-example/task"
 "testing"
)

type TestSuit struct {
 name string
 newTask NewTask
 size int
 times int
}

func TestTaskPoolRunImpl(t *testing.T) {

 testSuits := []TestSuit{
  //{
  // name: "minimal task pool",
  // newTask: func() task.Task { return mock.NewMinimalTask() },
  // size: 100,
  // times: 200,
  //},
 {
   name: "mock minimal task pool",
   newTask: func() task.Task { return mock.NewMockMinimalTask(ctrl) },
   size: 100,
   times: 200,
  },
 }

 for _, suit := range testSuits {
  t.Run(suit.name, func(t *testing.T) {
   var taskPool TaskPool = NewTaskPoolImpl(suit.newTask, suit.size)
   err := taskPool.Run(suit.size)
   assert.NoError(t, err)
  })
 }
}

We use this new test file for coverage testing

picture

It can be seen that the test results are the same, so what should we do when we want to achieve higher test coverage? We further modify the test

package pool

import (
 "errors"
 "github.com/golang/mock/gomock"
 "github.com/stretchr/testify/assert"
 "github.com/ultramesh/mock-example/mock"
 "github.com/ultramesh/mock-example/task"
 "testing"
)

type TestSuit struct {
 name string
 newTask NewTask
 size int
 times int
 isErr bool
}

func TestTaskPoolRunImpl_MinimalTask(t *testing.T) {

 ctrl := gomock.NewController(t)
 defer ctrl.Finish()

 testSuits := []TestSuit{
  //{
  // name: "minimal task pool",
  // newTask: func() task.Task { return mock.NewMinimalTask() },
  // size: 100,
  // times: 200,
  //},
  {
   name: "mock minimal task pool",
   newTask: func() task.Task { return mock.NewMockMinimalTask(ctrl) },
   size: 100,
   times: 200,
  },
  {
   name: "return err",
   newTask: func() task.Task {
    mockTask := mock.NewMockTask(ctrl)
  // 加入了返回错误的逻辑
    mockTask.EXPECT().Do(gomock.Any()).Return("", errors.New("return err")).AnyTimes()
    return mockTask
   },
   size: 100,
   times: 200,
   isErr: true,
  },
 }

 for _, suit := range testSuits {
  t.Run(suit.name, func(t *testing.T) {
   var taskPool TaskPool = NewTaskPoolImpl(suit.newTask, suit.size)
   err := taskPool.Run(suit.size)
   if suit.isErr {
    assert.Error(t, err)
   } else {
    assert.NoError(t, err)
   }
  })
 }
}

This way we can override the error processing logic

picture

Even we can cover all the statements in a more tricky way, change the testSuits in the code to the following

package pool

import (
 "errors"
 "github.com/golang/mock/gomock"
 "github.com/stretchr/testify/assert"
 "github.com/ultramesh/mock-example/mock"
 "github.com/ultramesh/mock-example/task"
 "testing"
)

type TestSuit struct {
 name string
 newTask NewTask
 size int
 times int
 isErr bool
}

func TestTaskPoolRunImpl_MinimalTask(t *testing.T) {

 ctrl := gomock.NewController(t)
 defer ctrl.Finish()

 strs := []string{"a""b""c"}
 count := 0
 size := 3
 rounds := 1

 testSuits := []TestSuit{
  //{
  // name: "minimal task pool",
  // newTask: func() task.Task { return mock.NewMinimalTask() },
  // size: 100,
  // times: 200,
  //},
  {
   name: "mock minimal task pool",
   newTask: func() task.Task { return mock.NewMockMinimalTask(ctrl) },
   size: 100,
   times: 200,
  },
  {
   name: "return err",
   newTask: func() task.Task {
    mockTask := mock.NewMockTask(ctrl)
    mockTask.EXPECT().Do(gomock.Any()).Return("", errors.New("return err")).AnyTimes()
    return mockTask
   },
   size: 100,
   times: 200,
   isErr: true,
  },
  {
   name: "check input and output",
   newTask: func() task.Task {
    mockTask := mock.NewMockTask(ctrl)
  // 这里我们通过Do的设置检查了mackTask.Do调用时候的入参以及调用次数
  // 通过Return来设置发生调用时的返回值
    mockTask.EXPECT().Do(count).Return(strs[count%3], nil).Times(rounds)
    count++
    return mockTask
   },
   size: size,
   times: size * rounds,
   isErr: false,
  },
 }
 var taskPool TaskPool
 for _, suit := range testSuits {
  t.Run(suit.name, func(t *testing.T) {
   taskPool = NewTaskPoolImpl(suit.newTask, suit.size)
   err := taskPool.Run(suit.times)
   if suit.isErr {
    assert.Error(t, err)
   } else {
    assert.NoError(t, err)
   }

  })
 }
}

This way we can cover all statements

picture

Think about the meaning of Mock

I discussed with some classmates before, why do we use mocks, and found that many classmates think that the way to write mocks is to agree on an interface, and then it can be convenient for testing when developing for an interface.

Because the actual implementation of the interface is not required, but the Minimal Implement of the mock can be used for unit testing. I think that's right, but at the same time I feel that mocking is more than that.

In my opinion, in the practice of interface-oriented development, you should always be sensitive to the input and output of the interface. Furthermore, when doing unit testing, you need to know that under a given use case and input, your The package will take what input to the interface method to use, call it a few times, and then what the return value might be, and what kind of return value affects you.

If you don't know about these, then I think or you should try and understand more, so that you can design more single test cases through mocking as much as possible, do more and careful inspection, and improve the coverage of test code rate to ensure the completeness of the module function.

picture



Reference link: https://www.jb51.net/article/188480.htm

For more technical articles or video tutorials related to the Go language, please follow this official account to obtain and view, thank you for your support and trust!