宋宇宏 發表於 2025-11-14 09:21:15

go语言中GoMock的实现

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">安装</a></li><li><a href="#_label1">开始</a></li><li><a href="#_label2">使用参数匹配器</a></li></ul></div><p>GoMock是一个Go框架。它与内置的测试包整合得很好,并在单元测试时提供了灵活性。正如我们所知,对具有外部资源(数据库、网络和文件)或依赖关系的代码进行单元测试总是很麻烦。</p>
<p class="maodian"><a name="_label0"></a></p><h2>安装</h2>
<p>为了使用GoMock,我们需要安装gomock包github.com/golang/mock/gomock和mockgen代码生成工具github.com/golang/mock/mockgen。使用这个命令行:</p>
<div class="jb51code"><pre class="brush:go;">go get github.com/golang/mock/gomock
go get github.com/golang/mock/mockgen
</pre></div>
<p>GoMock的使用遵循四个基本步骤:</p>
<ul><li>使用mockgen为你想模拟的接口生成一个模拟对象。</li><li>在测试部分,创建一个gomock.Controller的实例,并把它传递给你的mock对象的构造函数以获得一个mock对象。</li><li>在mock上调用EXPECT()来设置期望值和返回值。</li><li>在模拟控制器上调用Finish()来断言模拟对象的期望。</li></ul>
<p class="maodian"><a name="_label1"></a></p><h2>开始</h2>
<p>让我们创建一个这样的文件夹(本代码在 go1.16.15 版本下执行)</p>
<div class="jb51code"><pre class="brush:plain;">gomock_test
├── doer
│ └── doer.go
├── mocks
│ └── mock_doer.go
└── user
├── user.go
└── user_test.go
</pre></div>
<p>doer/doer.go</p>
<div class="jb51code"><pre class="brush:go;">package doer

type Doer interface {
DoSomething(int, string) error
SaySomething(string) string
}
</pre></div>
<p>那么这里是我们在模拟Doer接口时要测试的代码。</p>
<p>user/user.go</p>
<div class="jb51code"><pre class="brush:go;">package user

import "gomock_test/doer"

const (
        filtered   = "OK"
        unfiltered = "spam"
        Nice       = "nice"
        Bad      = "bad"
)

type User struct {
        // struct while mocking the doer interface
        Doer doer.Doer
}

// method Use using it
func (u *User) Use() error {
        return u.Doer.DoSomething(123, "Hello GoMock")
}

func (u *User) SaySomething(num int) string {
        if num == 3 {
                return u.Doer.SaySomething(unfiltered)
        }
        return u.Doer.SaySomething(filtered)
}

type Student struct{}

func (s *Student) DoSomething(_ int, _ string) error {
        panic("not implemented") // TODO: Implement
}

func (s *Student) SaySomething(kata string) string {
        if kata == filtered {
                return Nice
        }
        return Bad
}
</pre></div>
<p>我们将把Doer的模拟放在一个包mocks中。我们首先创建一个包含我们的模拟实现的目录mocks,然后在doer包上运行mockgen:</p>
<div class="jb51code"><pre class="brush:go;">mockgen -destination=../mocks/mock_doer.go -package=mocks gomock_test/doer Doer
</pre></div>
<blockquote><p>NOTE: 在执行这步的时候,会报错:Failed to format generated source code: mocks/mock_doer.go:5:15: expected &lsquo;;&rsquo;, found &#39;.&rsquo; 这个时候,我们只需要将打印出来的代码复制到我们对应的文件中即可。</p></blockquote>
<p>当有大量的接口/包需要模拟时,为每个包和接口运行mockgen是一种乌托邦。为了缓解这个问题,可以将mockgen命令与go:generate放在一起。</p>
<div class="jb51code"><pre class="brush:go;">go:generate mockgen -destination=../mocks/mock_doer.go -package=mocks gomock_test/doer Doer
</pre></div>
<p>我们必须自己创建目录模拟,因为GoMock不会为我们这样做,而是会以错误退出。</p>
<ul><li>destination=&hellip;/mocks/mock_doer.go : 把生成的mocks放在这个路径下。</li><li>-package=mocks : 把生成的mocks放在这个包里</li><li>gomock_test/doer : 为这个包生成mocks。</li><li>Doer : 为这个接口生成mocks(必填),因为我们需要指定哪个接口来生成mocks。(如果需要的话,可以用逗号分隔的列表来指定多个接口。例如,Doer1, Doer2)</li></ul>
<p>因为我们对mockgen的调用在我们的项目中放置了一个文件mocks/mock_doer.go。这就是这样一个生成的mock实现的样子:</p>
<div class="jb51code"><pre class="brush:go;">// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/timliudream/go-test/gomock_test/doer (interfaces: Doer)

// Package github.com/timliudream/go-test/gomock_test/mocks is a generated GoMock package.
package mocks

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

// MockDoer is a mock of Doer interface.
type MockDoer struct {
        ctrl   *gomock.Controller
        recorder *MockDoerMockRecorder
}

// MockDoerMockRecorder is the mock recorder for MockDoer.
type MockDoerMockRecorder struct {
        mock *MockDoer
}

// NewMockDoer creates a new mock instance.
func NewMockDoer(ctrl *gomock.Controller) *MockDoer {
        mock := &amp;MockDoer{ctrl: ctrl}
        mock.recorder = &amp;MockDoerMockRecorder{mock}
        return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDoer) EXPECT() *MockDoerMockRecorder {
        return m.recorder
}

// DoSomething mocks base method.
func (m *MockDoer) DoSomething(arg0 int, arg1 string) error {
        m.ctrl.T.Helper()
        ret := m.ctrl.Call(m, "DoSomething", arg0, arg1)
        ret0, _ := ret.(error)
        return ret0
}

// DoSomething indicates an expected call of DoSomething.
func (mr *MockDoerMockRecorder) DoSomething(arg0, arg1 interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoSomething", reflect.TypeOf((*MockDoer)(nil).DoSomething), arg0, arg1)
}

// SaySomething mocks base method.
func (m *MockDoer) SaySomething(arg0 string) string {
        m.ctrl.T.Helper()
        ret := m.ctrl.Call(m, "SaySomething", arg0)
        ret0, _ := ret.(string)
        return ret0
}

// SaySomething indicates an expected call of SaySomething.
func (mr *MockDoerMockRecorder) SaySomething(arg0 interface{}) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaySomething", reflect.TypeOf((*MockDoer)(nil).SaySomething), arg0)
}
</pre></div>
<p>接下来,我们在测试中定义一个模拟控制器。一个模拟控制器负责跟踪和断言其相关模拟对象的期望。我们可以通过传递一个*testing.T类型的值给它的构造函数来获得一个模拟控制器,并使用它来构造一个Doer接口的模拟对象。</p>
<div class="jb51code"><pre class="brush:go;">//core of gomock
mockCtrl := gomock.NewController(t)
//used to trigger final assertions. if its ignored, mocking assertions will never fail
defer mockCtrl.Finish()
// create a new mock object, passing the controller instance as parameter
// for a newly created mock object it will accept any input and outpuite
// need to define its behavior with the method expect
mockDoer := mocks.NewMockDoer(mockCtrl)
</pre></div>
<p class="maodian"><a name="_label2"></a></p><h2>使用参数匹配器</h2>
<p>在GoMock中,一个参数可以被期望有一个固定的值,也可以被期望与一个谓词(称为匹配器)相匹配。匹配器用于表示被模拟方法的预期参数范围。下列匹配器在Gomock中被预先定义了:</p>
<ul><li>gomock.Any() : 匹配任何值(任何类型)。</li><li>gomock.Eq(x) : 使用反射来匹配与x深度相等的值。</li><li>gomock.Nil() : 匹配nil</li></ul>
<p>user/user_test.go</p>
<div class="jb51code"><pre class="brush:go;">package user

import (
        "fmt"
        "github.com/golang/mock/gomock"
        "gomock_test/mocks"
        "testing"
)

func TestUse(t *testing.T) {
        //core of gomock
        mockCtrl := gomock.NewController(t)
        //used to trigger final assertions. if its ignored, mocking assertions will never fail
        defer mockCtrl.Finish()
        // create a new mock object, passing the controller instance as parameter
        // for a newly created mock object it will accept any input and outpuite
        // need to define its behavior with the method expect
        mockDoer := mocks.NewMockDoer(mockCtrl)
        testUser := &amp;User{Doer: mockDoer}
        //
        // Expect Do to be called once with 123 and "Hello GoMock" as parameters, and return nil from the mocked call.
        mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(nil).Times(1)
        fmt.Println(testUser.Use())
        fmt.Println()
}

func TestUser_SaySomething(t *testing.T) {
        mockCtrl := gomock.NewController(t)
        defer mockCtrl.Finish()
        mockDoer := mocks.NewMockDoer(mockCtrl)
        user := User{
                Doer: mockDoer,
        }
        type args struct {
                num int
        }
        tests := []struct {
                name    string
                args    args
                want    string
                expectfunc()
                wantErr bool
        }{
                {
                        name: "Positive test case 1",
                        expect: func() {
                                mockDoer.EXPECT().SaySomething("spam").Return("bad")
                        },
                        args:    args{num: 3},
                        wantErr: false,
                        want:    "bad",
                },
        }
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
                        tt.expect()
                        if got := user.SaySomething(tt.args.num); (got != tt.want) != tt.wantErr {
                                fmt.Println("gott :", got)
                                t.Errorf("User.SaySomething() = %v, want %v", got, tt.want)
                        }
                })
        }
}
</pre></div>
<p>而单元测试的结果将是这样的:</p>
<div class="jb51code"><pre class="brush:plain;">=== RUN   TestUser_SaySomething
=== RUN   TestUser_SaySomething/Positive_test_case_1
--- PASS: TestUser_SaySomething (0.00s)
    --- PASS: TestUser_SaySomething/Positive_test_case_1 (0.00s)
PASS
ok      github.com/tokopedia/go_learning/udemy/pzn/gomock_test/user   1.100s
</pre></div>
頁: [1]
查看完整版本: go语言中GoMock的实现