前面的小节我们使用了自行模拟的方式。当你需要面对很多的接口时,这么干会变得极为麻烦且极易发生错误。这是自动化测试的意义所在。本节我们使用 github.com/golang/mock/gomock ,该库提供了一组模拟对象,可以与接口测试结合使用。

实践

获取第三方库:

go get github.com/golang/mock/

建立 interface.go:

package mockgen

type GetSetter interface {
    Set(key, val string) error
    Get(key string) (string, error)
}

运行命令行建立 mocks.go:

mockgen -destination internal/mocks.go -package internal
github.com/agtorre/go-cookbook/chapter8/mockgen GetSetter
// Automatically generated by MockGen. DO NOT EDIT!
// Source: github.com/agtorre/go-cookbook/chapter8/mockgen (interfaces: GetSetter)

package internal

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

// Mock of GetSetter interface
type MockGetSetter struct {
    ctrl     *gomock.Controller
    recorder *_MockGetSetterRecorder
}

// Recorder for MockGetSetter (not exported)
type _MockGetSetterRecorder struct {
    mock *MockGetSetter
}

func NewMockGetSetter(ctrl *gomock.Controller) *MockGetSetter {
    mock := &MockGetSetter{ctrl: ctrl}
    mock.recorder = &_MockGetSetterRecorder{mock}
    return mock
}

func (_m *MockGetSetter) EXPECT() *_MockGetSetterRecorder {
    return _m.recorder
}

func (_m *MockGetSetter) Get(_param0 string) (string, error) {
    ret := _m.ctrl.Call(_m, "Get", _param0)
    ret0, _ := ret[0].(string)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

func (_mr *_MockGetSetterRecorder) Get(arg0 interface{}) *gomock.Call {
    return _mr.mock.ctrl.RecordCall(_mr.mock, "Get", arg0)
}

func (_m *MockGetSetter) Set(_param0 string, _param1 string) error {
    ret := _m.ctrl.Call(_m, "Set", _param0, _param1)
    ret0, _ := ret[0].(error)
    return ret0
}

func (_mr *_MockGetSetterRecorder) Set(arg0, arg1 interface{}) *gomock.Call {
    return _mr.mock.ctrl.RecordCall(_mr.mock, "Set", arg0, arg1)
}

建立 exec.go:

package mockgen

// Controller 这个结构体演示了一种初始化接口的方式
type Controller struct {
    GetSetter
}

// GetThenSet 检查值是否已设置。如果没有设置就将其设置
func (c *Controller) GetThenSet(key, value string) error {
    val, err := c.Get(key)
    if err != nil {
        return err
    }

    if val != value {
        return c.Set(key, value)
    }
    return nil
}

建立 interface_test.go:

package mockgen

import (
    "errors"
    "testing"

    "github.com/agtorre/go-cookbook/chapter8/mockgen/internal"
    "github.com/golang/mock/gomock"
)

func TestExample(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockGetSetter := internal.NewMockGetSetter(ctrl)

    var k string
    mockGetSetter.EXPECT().Get("we can put anything here!").Do(func(key string) {
        k = key
    }).Return("", nil)

    customError := errors.New("failed this time")

    mockGetSetter.EXPECT().Get(gomock.Any()).Return("", customError)

    if _, err := mockGetSetter.Get("we can put anything here!"); err != nil {
        t.Errorf("got %#v; want %#v", err, nil)
    }
    if k != "we can put anything here!" {
        t.Errorf("bad key")
    }

    if _, err := mockGetSetter.Get("key"); err == nil {
        t.Errorf("got %#v; want %#v", err, customError)
    }
}

建立 exec_test.go:

package mockgen

import (
    "errors"
    "testing"

    "github.com/agtorre/go-cookbook/chapter8/mockgen/internal"
    "github.com/golang/mock/gomock"
)

func TestController_Set(t *testing.T) {
    tests := []struct {
        name         string
        getReturnVal string
        getReturnErr error
        setReturnErr error
        wantErr      bool
    }{
        {"get error", "value", errors.New("failed"), nil, true},
        {"value match", "value", nil, nil, false},
        {"no errors", "not set", nil, nil, false},
        {"set error", "not set", nil, errors.New("failed"), true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            ctrl := gomock.NewController(t)
            defer ctrl.Finish()

            mockGetSetter := internal.NewMockGetSetter(ctrl)
            mockGetSetter.EXPECT().Get("key").AnyTimes().Return(tt.getReturnVal, tt.getReturnErr)
            mockGetSetter.EXPECT().Set("key", gomock.Any()).AnyTimes().Return(tt.setReturnErr)

            c := &Controller{
                GetSetter: mockGetSetter,
            }
            if err := c.GetThenSet("key", "value"); (err != nil) != tt.wantErr {
                t.Errorf("Controller.Set() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

说明

生成的模拟对象允许测试预定的参数,调用函数的次数以及返回的内容,并且允许我们设置其他工作流程。interface_test.go文件展示了在线调用它们时使用模拟对象的一些示例。 通常,测试看起来更像exec_test.go,我们希望拦截由实际代码执行的接口函数调用,并在测试时更改它们的行为。

exec_test.go文件还展示了如何在表驱动的测试环境中使用模拟对象。Any()函数意味着模拟函数可以被调用零次或多次,这对于代码提前终止的情况非常有用。

示例演示的最后一个技巧是将模拟对象粘贴到内部包中。当需要模拟在自己之外的包中声明的函数时,这非常有用。 这允许在非test.go文件中定义这些方法,但不允许将它们导出到库的情况。通常,使用与当前编写的测试相同的包名称将模拟对象粘贴到test.go文件中更容易。

最后编辑: kuteng  文档更新时间: 2021-01-03 15:03   作者:kuteng