注:本文由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
其他参考
- Advanced Testing in Go
- 了解下测试驱动开发(Test-driven development, TDD)?
我的《测试驱动的嵌入式C开发》读书笔记