注:本文由linstring原创,博客地址:https://blog.csdn.net/lin_strong
转载请注明出处
第一部分链接:
https://blog.csdn.net/lin_strong/article/details/109012560
文章目录
- 如何插入测试点/解开依赖
-
- For 普通函数
-
- 场景
- 插入测试点
- 如何GoMock非接口
- 尝试下Testify's Mock?
- For 外部依赖
- For 对象内部依赖
- For 工厂模式
- 其他测试技巧
-
- 作为Process来测试
- 测试时序
- Http test
- I/O test
- 其他参考
如何插入测试点/解开依赖
For 普通函数
很多函数都是独立的形式,比如上面的
场景
假设我们在测的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 } } |
依赖于一个网络服务(其他同理):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 } |
由于测试环境中无法连接到,或者耗时太长,返回不可控等原因,我们不能真正调用它。而我们也不想改变被测函数的签名,被依赖的函数也不能随便动,因为还有好几个其他地方依赖着它。
插入测试点
这时,可以这么改被依赖的函数以方便注入依赖:
1 2 3 4 5 6 7 8 9 10 11 | 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类型。这也好解决,只要加个有这签名的方法的接口:
1 2 3 | type ExampleServiceInterface interface {<!-- --> Do(req *ExampleRequest) (*ExampleResponse, error) } |
然后生成:
1 | mockgen -source=service.go -destination=mock/ExampleServiceFunc.go |
这样,测试用例就长成这样(并不完善,纯粹为示例设置打桩点):
1 2 3 4 5 6 7 8 9 10 11 12 | 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生成工具
1 2 3 4 | $ 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | // 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的概念类似,功能上略微弱些。用其写此测试用例的话:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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接口么)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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") } |
然后我们再写一个主控类,它有一个方法是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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的签名直接传入接口。那为了能够测试它,可以依样画瓢设个桩:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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文件和被测文件在同一个目录下,是可以直接访问内部字段的,所以对应的测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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的工厂:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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对象:
1 | mockgen -source=factory.go -destination=mock/Factory.go |
然后比如maincontroller那有个RequestNew方法。为了安全,不允许直接传入Controller来注册,只能说明需要增加什么类型的控制器,然后返回给你注册好并启动好的Controller。这时内部就需要用到工厂来生产对象了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 } |
为了测试它,不能让
1 2 3 4 5 6 7 8 9 10 11 12 | 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) …… } |
这下我们的测试能够从头控制到尾了??:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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的行为
1 2 3 4 | func Crasher() {<!-- --> fmt.Println("Going down in flames!") os.Exit(1) } |
为了测试这个代码,可以把测试生成的二进制文件本身作为一个subprocess来调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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可测试的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | …… // 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开发》读书笔记