Golang单元测试快速上手(三) 高级技巧

注:本文由linstring原创,博客地址:https://blog.csdn.net/lin_strong

转载请注明出处

第一部分链接:
https://blog.csdn.net/lin_strong/article/details/109012560


如何插入测试点/解开依赖

For 普通函数

很多函数都是独立的形式,比如上面的TurnOnSequentially,而不是作为接口的某一方法。如果要测试的对象直接依赖于独立的函数,Mock起来就较为困难了。有些语言可以直接替换掉依赖的函数(比如C语言中使用链接时替代/预处理器替代,解释型语言中直接替掉,Java中也可以直接替),而在Go中则较为困难。但我们可以通过函数指针的方式来解开依赖。

场景

假设我们在测的函数:

package setstubdemo

import (
	"fmt"
	"go_test_demo/service"
	"time"
)

func AFunction() int{
	resp, err := service.ExampleService(&service.ExampleRequest{When: time.Now()})
	if err == nil && resp != nil{
		return resp.Rst
	}else {
		fmt.Printf("Error: resp %#+v, err %v\n", resp, err)
		return 0
	}
}

依赖于一个网络服务(其他同理):

package service

import "time"

type ExampleRequest struct {
   When time.Time
}

type ExampleResponse struct {
   Rst int
}

func ExampleService(req *ExampleRequest) (*ExampleResponse, error){
   time.Sleep(time.Minute * 10)
   return &ExampleResponse{Rst: 10}, nil
}

由于测试环境中无法连接到,或者耗时太长,返回不可控等原因,我们不能真正调用它。而我们也不想改变被测函数的签名,被依赖的函数也不能随便动,因为还有好几个其他地方依赖着它。

插入测试点

这时,可以这么改被依赖的函数以方便注入依赖:

type ExampleServiceFun func(req *ExampleRequest) (*ExampleResponse, error)

var ExampleServiceStub ExampleServiceFun

func ExampleService(req *ExampleRequest) (*ExampleResponse, error){
   if ExampleServiceStub != nil {
      return ExampleServiceStub(req)
   }
   time.Sleep(time.Minute * 10)
   return &ExampleResponse{Rst: 10}, nil
}

这样我们即不会改变函数原来行为,也还可以继续享受IDE方便的提示功能(如果把ExampleService设为函数指针的话就失去了代码提示的便利,虽然也能打桩),还能很轻松的移除注入的依赖(通过将stub设为nil)。

如何GoMock非接口

现在又产生了一个问题,GoMock不支持Mock Func类型。这也好解决,只要加个有这签名的方法的接口:

type ExampleServiceInterface interface {
   Do(req *ExampleRequest) (*ExampleResponse, error)
}

然后生成:

mockgen -source=service.go -destination=mock/ExampleServiceFunc.go

这样,测试用例就长成这样(并不完善,纯粹为示例设置打桩点):

func TestAFunctionWithGoMock(t *testing.T) {
   ctrl := gomock.NewController(t)
   defer ctrl.Finish()
   mockFunc := mock_service.NewMockExampleServiceInterface(ctrl)
   service.ExampleServiceStub = mockFunc.Do
   defer func() {service.ExampleServiceStub = nil}()
   mockFunc.EXPECT().Do(gomock.Any()).Return(&service.ExampleResponse{Rst: 10}, nil)

   got := AFunction()

   assert.Equal(t, 10, got)
}

尝试下Testify’s Mock?

但是多写一个接口定义还是略显繁琐,这种情况我个人更喜欢直接使用testify的mock,因为其mock生成工具mockery支持mock函数,而且用起来更顺手:

$ mockery --name=ExampleServiceFun
11 Oct 20 17:53 CST INF Starting mockery dry-run=false version=2.2.1
11 Oct 20 17:53 CST INF Walking dry-run=false version=2.2.1
11 Oct 20 17:53 CST INF Generating mock dry-run=false interface=ExampleServiceFun qualified-name=go_test_demo/service version=2.2.1

这样就直接自动为其生成了mock对象于mocks/ExampleServiceFun.go

// Code generated by mockery v2.2.1. DO NOT EDIT.

package mocks

import (
	service "go_test_demo/service"

	mock "github.com/stretchr/testify/mock"
)

// ExampleServiceFun is an autogenerated mock type for the ExampleServiceFun type
type ExampleServiceFun struct {
	mock.Mock
}

// Execute provides a mock function with given fields: req
func (_m *ExampleServiceFun) Execute(req *service.ExampleRequest) (*service.ExampleResponse, error) {
	ret := _m.Called(req)

	var r0 *service.ExampleResponse
	if rf, ok := ret.Get(0).(func(*service.ExampleRequest) *service.ExampleResponse); ok {
		r0 = rf(req)
	} else {
		if ret.Get(0) != nil {
			r0 = ret.Get(0).(*service.ExampleResponse)
		}
	}

	var r1 error
	if rf, ok := ret.Get(1).(func(*service.ExampleRequest) error); ok {
		r1 = rf(req)
	} else {
		r1 = ret.Error(1)
	}

	return r0, r1
}

testify的mock和gomock的概念类似,功能上略微弱些。用其写此测试用例的话:

package setstubdemo

import (
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"go_test_demo/service"
	"go_test_demo/service/mocks"
	"testing"
)

func TestAFunctionWithTestifyMock(t *testing.T) {
	mockFunc := new(mocks.ExampleServiceFun)
	defer mockFunc.AssertExpectations(t)
	service.ExampleServiceStub = mockFunc.Execute
	defer func() { service.ExampleServiceStub = nil }()
	mockFunc.On("Execute", mock.Anything).Return(&service.ExampleResponse{Rst: 10}, nil)

	got := AFunction()

	assert.Equal(t, 10, got)
}

可以看到,testify的mock对象是独立verify的,不支持设置相互间顺序,但其实很多情况下够用了。

For 外部依赖

对于想解开对外部函数依赖的情况,换句话说,无法直接修改被依赖函数的代码的时候。

建议加一个中间层,改成依赖中间层,而不是直接依赖外部函数,这样就可以随意注入依赖了。

For 对象内部依赖

其实方法论已经有了,剩下就是活学活用的问题。

为了示例,我们先假装实现一个LedBoard(还记得它实现了Controller接口么)

package controller

type LedBoard struct {
   name string
}

func NewLedBoard(name string) Controller {
   return &LedBoard{name: name}
}

func (l *LedBoard) PowerUp() error {
   panic("implement me")
}

func (l *LedBoard) Open(num int) error {
   panic("implement me")
}

func (l *LedBoard) Close(num int) error {
   panic("implement me")
}

func (l *LedBoard) Count() int {
   panic("implement me")
}

func (l *LedBoard) PowerDown() error {
   panic("implement me")
}

然后我们再写一个主控类,它有一个方法是这样的:

package maincontroller

import "go_test_demo/controller"

type MainController struct {
}

func NewMainController() *MainController {
   return &MainController{}
}

func (m *MainController) Blink() {
   ctrler := controller.NewLedBoard("dummy")
   ctrler.PowerUp()
   // Do something
   ctrler.PowerDown()
}

并且你确定不应该修改Blink的签名直接传入接口。那为了能够测试它,可以依样画瓢设个桩:

package maincontroller

import "go_test_demo/controller"

type MainController struct {
   blinkCtrlStub controller.Controller
}

func NewMainController() *MainController {
   return &MainController{}
}

func (m *MainController) Blink() {
   var ctrler controller.Controller
   if m.blinkCtrlStub == nil {
      ctrler = controller.NewLedBoard("dummy")
   }else {
      ctrler = m.blinkCtrlStub
   }
   ctrler.PowerUp()
   // Do something
   ctrler.PowerDown()
}

而且这个桩很安全,因为外部访问不到。而由于Test文件和被测文件在同一个目录下,是可以直接访问内部字段的,所以对应的测试:

package maincontroller

import (
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	"go_test_demo/controller"
	mock_controller "go_test_demo/controller/mock"
	"testing"
)

func TestMainController_Blink(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	mockCtrler := mock_controller.NewMockController(ctrl)
	mc := NewMainController()
	mc.blinkCtrlStub = mockCtrler
	gomock.InOrder(
		mockCtrler.EXPECT().PowerUp(),
		// others
		mockCtrler.EXPECT().PowerDown(),
	)

	mc.Blink()
}

For 工厂模式

那再玩花点,如果方法内部使用的是工厂模式,甚至可以玩玩用mock生产mock。

比如controller的工厂:

package controller

type Type int

const (
   LEDBoard Type = 0
)

type FactoryFunc func(t Type) Controller

func (f FactoryFunc) New(t Type) Controller {
   return f(t)
}

type Factory interface {
   New(t Type) Controller
}

func DefaultFactory(t Type) Controller {
   switch t {
   case LEDBoard:
      return NewLedBoard("Ha")
   default:
      return nil
   }
}

咱给它搞个mock对象:

mockgen -source=factory.go -destination=mock/Factory.go

然后比如maincontroller那有个RequestNew方法。为了安全,不允许直接传入Controller来注册,只能说明需要增加什么类型的控制器,然后返回给你注册好并启动好的Controller。这时内部就需要用到工厂来生产对象了:

func (m *MainController)RequestNew(t controller.Type) controller.Controller {
   var fac controller.Factory = controller.FactoryFunc(controller.DefaultFactory)
   ctrler := fac.New(t)
   if ctrler == nil {
      return nil
   }
   err := ctrler.PowerUp()
   if err != nil {
      fmt.Printf("Error: [RequestNew] PowerUp fail, err %v\n", err)
      return nil
   }
   ctrlservicetd.TurnOnSequentially(ctrler)
   return ctrler
}

为了测试它,不能让RequestNew方法直接使用默认的工厂,而应该插个桩:

type MainController struct {
   blinkCtrlStub controller.Controller
   fac controller.Factory
}

func NewMainController() *MainController {
   return &MainController{fac: controller.FactoryFunc(controller.DefaultFactory)}
}
func (m *MainController)RequestNew(t controller.Type) controller.Controller {
   ctrler := m.fac.New(t)
   ……
}

这下我们的测试能够从头控制到尾了😄:

func TestMainController_RequestNew(t *testing.T) {
   ctrl := gomock.NewController(t)
   defer ctrl.Finish()
   mockFac := mock_controller.NewMockFactory(ctrl)
   mockCtrler := mock_controller.NewMockController(ctrl)
   mockFac.EXPECT().New(controller.Type(4)).Return(mockCtrler)
   mockCtrler.EXPECT().Count().Return(3).AnyTimes()
   gomock.InOrder(
      mockCtrler.EXPECT().PowerUp(),
      mockCtrler.EXPECT().Open(0).Return(nil),
      mockCtrler.EXPECT().Open(1).Return(nil),
      mockCtrler.EXPECT().Open(2).Return(nil),
   )
   mc := NewMainController()
   mc.fac = mockFac

   got := mc.RequestNew(controller.Type(4))

   assert.Equal(t, got, mockCtrler)
}

其他测试技巧

作为Process来测试

有时,会想要测试一个process而不是一个function的行为

func Crasher() {
   fmt.Println("Going down in flames!")
   os.Exit(1)
}

为了测试这个代码,可以把测试生成的二进制文件本身作为一个subprocess来调用:

func TestCrasher(t *testing.T) {
   if os.Getenv("BE_CRASHER") == "1" {
      Crasher()
      return
   }
   cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
   cmd.Env = append(os.Environ(), "BE_CRASHER=1")
   err := cmd.Run()
   if e, ok := err.(*exec.ExitError); ok && !e.Success() {
      return
   }
   t.Fatalf("process ran with err %v, want exit status 1", err)
}

From:https://talks.golang.org/2014/testing.slide#21

测试时序

和时序有关的模块较难测试,经常选择不测试。。。

为了控制时间,代码不能直接依赖于time库,而要依赖于抽象。
https://github.com/juju/ratelimit 有使sleep可测试的示例

……
// NewBucketWithClock is identical to NewBucket but injects a testable clock
// interface.
func NewBucketWithClock(fillInterval time.Duration, capacity int64, clock Clock) *Bucket {
   return NewBucketWithQuantumAndClock(fillInterval, capacity, 1, clock)
}
……
// Wait takes count tokens from the bucket, waiting until they are
// available.
func (tb *Bucket) Wait(count int64) {
   if d := tb.Take(count); d > 0 {
      tb.clock.Sleep(d)
   }
}

// WaitMaxDuration is like Wait except that it will
// only take tokens from the bucket if it needs to wait
// for no greater than maxWait. It reports whether
// any tokens have been removed from the bucket
// If no tokens have been removed, it returns immediately.
func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool {
   d, ok := tb.TakeMaxDuration(count, maxWait)
   if d > 0 {
      tb.clock.Sleep(d)
   }
   return ok
}
……
// Clock represents the passage of time in a way that
// can be faked out for tests.
type Clock interface {
   // Now returns the current time.
   Now() time.Time
   // Sleep sleeps for at least the given duration.
   Sleep(d time.Duration)
}

// realClock implements Clock in terms of standard time functions.
type realClock struct{}

// Now implements Clock.Now by calling time.Now.
func (realClock) Now() time.Time {
   return time.Now()
}

// Now implements Clock.Sleep by calling time.Sleep.
func (realClock) Sleep(d time.Duration) {
   time.Sleep(d)
}

Http test

httptest包提供了用于http测试的工具:
https://godoc.org/net/http/httptest

I/O test

如果要mock io.Reader和io.Writer的话:
https://godoc.org/testing/iotest

其他参考

  1. Advanced Testing in Go
  2. 了解下测试驱动开发(Test-driven development, TDD)?
    我的《测试驱动的嵌入式C开发》读书笔记
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值