前面我们完成了最麻烦的数据层的单元测试,今天我们来看看单元测试中最容易做的一层,数据逻辑层,也就是我们通常说的 service
或者 biz
等,是描述具体业务逻辑的地方,这一层包含我们业务最重要的逻辑。
所以它的测试非常重要,通常它测试的通过就意味着你的业务逻辑能正常运行了。
而如何对它做单元测试呢? 因为,这一层的依赖主要来源于数据层,通常这一层会调用数据层的接口来获取或操作数据。 由于我们之前对于数据层已经做了单元测试,所以这一次,我们需要 mock 的不是数据库了,而是数据层。
Golang 提供了 github.com/golang/mock 来实现 mock 接口的操作,本文就是使用它来完成我们的单元测试。
准备工作
安装 go install github.com/golang/mock/mockgen@v1.6.0
基本 case 代码
首先我们还是基于上一次的例子,这里给出上一次例子中所用到的接口
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
| package service
import ( "context" "fmt"
"go-demo/m/unit-test/entity" )
type UserRepo interface { AddUser(ctx context.Context, user *entity.User) (err error) DelUser(ctx context.Context, userID int) (err error) GetUser(ctx context.Context, userID int) (user *entity.User, exist bool, err error) }
type UserService struct { userRepo UserRepo }
func NewUserService(userRepo UserRepo) *UserService { return &UserService{userRepo: userRepo} }
func (us *UserService) AddUser(ctx context.Context, username string) (err error) { if len(username) == 0 { return fmt.Errorf("username not specified") } return us.userRepo.AddUser(ctx, &entity.User{Username: username}) }
func (us *UserService) GetUser(ctx context.Context, userID int) (user *entity.User, err error) { userInfo, exist, err := us.userRepo.GetUser(ctx, userID) if err != nil { return nil, err } if !exist { return nil, fmt.Errorf("user %d not found", userID) } return userInfo, nil }
|
可以看到我们的目标很明确,就是需要 mock 掉 UserRepo
接口的几个方法,就可以测试我们 AddUser
和 GetUser
方法了
生成 mock 接口
使用 mockgen
命令可以生成我们所需要的 mock 接口
1
| mockgen -source=./service/user.go -destination=./mock/user_mock.go -package=mock
|
参数名称都很好理解,我这边不赘述了。命令执行完成之后,会在 destination
生成对于的 mock 接口,就可以使用了。
生成的代码大致如下面的样子,可以简单瞄一眼:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
|
package mock
import ( context "context" entity "go-demo/m/unit-test/entity" reflect "reflect"
gomock "github.com/golang/mock/gomock" )
type MockUserRepo struct { ctrl *gomock.Controller recorder *MockUserRepoMockRecorder }
type MockUserRepoMockRecorder struct { mock *MockUserRepo }
func NewMockUserRepo(ctrl *gomock.Controller) *MockUserRepo { mock := &MockUserRepo{ctrl: ctrl} mock.recorder = &MockUserRepoMockRecorder{mock} return mock }
func (m *MockUserRepo) EXPECT() *MockUserRepoMockRecorder { return m.recorder }
func (m *MockUserRepo) AddUser(ctx context.Context, user *entity.User) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AddUser", ctx, user) ret0, _ := ret[0].(error) return ret0 }
func (mr *MockUserRepoMockRecorder) AddUser(ctx, user interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUser", reflect.TypeOf((*MockUserRepo)(nil).AddUser), ctx, user) }
func (m *MockUserRepo) DelUser(ctx context.Context, userID int) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DelUser", ctx, userID) ret0, _ := ret[0].(error) return ret0 }
func (mr *MockUserRepoMockRecorder) DelUser(ctx, userID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelUser", reflect.TypeOf((*MockUserRepo)(nil).DelUser), ctx, userID) }
func (m *MockUserRepo) GetUser(ctx context.Context, userID int) (*entity.User, bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetUser", ctx, userID) ret0, _ := ret[0].(*entity.User) ret1, _ := ret[1].(bool) ret2, _ := ret[2].(error) return ret0, ret1, ret2 }
func (mr *MockUserRepoMockRecorder) GetUser(ctx, userID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockUserRepo)(nil).GetUser), ctx, userID) }
|
编写单元测试
gomock
的单元测试编写起来也很方便,只需要调用 EXPECT()
方法,将需要 mock 的接口对应需要的返回值就可以了。我们直接来看例子:
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
| package service
import ( "context" "testing"
"github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "go-demo/m/unit-test/entity" "go-demo/m/unit-test/mock" )
func TestUserService_AddUser(t *testing.T) { ctl := gomock.NewController(t) defer ctl.Finish()
mockUserRepo := mock.NewMockUserRepo(ctl) userInfo := &entity.User{Username: "LinkinStar"} mockUserRepo.EXPECT().AddUser(gomock.Any(), gomock.Any()).Return(nil)
userService := NewUserService(mockUserRepo) err := userService.AddUser(context.TODO(), userInfo.Username) assert.NoError(t, err) }
func TestUserService_GetUser(t *testing.T) { ctl := gomock.NewController(t) defer ctl.Finish()
userID := 1 username := "LinkinStar"
mockUserRepo := mock.NewMockUserRepo(ctl) mockUserRepo.EXPECT().GetUser(context.TODO(), userID).Return(&entity.User{ ID: userID, Username: username, }, true, nil)
userService := NewUserService(mockUserRepo) userInfo, err := userService.GetUser(context.TODO(), userID) assert.NoError(t, err) assert.Equal(t, username, userInfo.Username) }
|
与之前一样,我们依旧使用 github.com/stretchr/testify
做断言来验证最终结果。可以看到,单元测试编写起来并不难。
优化
当然,如果我们每次修改接口或者新增接口都需要重新执行一次命令,一个文件还好,当有很多文件的时候肯定是非常困难的。所以我们需要使用 go:generate 来优化一下。
我们可以在需要 mock 的接口上方加入注释(注意这里写的路径要和实际路径相符合):
1 2 3 4 5 6
| type UserRepo interface { AddUser(ctx context.Context, user *entity.User) (err error) DelUser(ctx context.Context, userID int) (err error) GetUser(ctx context.Context, userID int) (user *entity.User, exist bool, err error) }
|
然后只需要使用命令
就可以生成全部的 mock 嘞,所以及时文件很多,只需要利用好 go:generate 也能一次搞定
mockgen
比如针对指定参数,我们偷懒可以都用 Any,但常常还需要用 gomock.Eq()
或 gomock.Not("Sam")
有关 gomock 还有很多方法在测试的使用也很有用,详细的文档在:https://pkg.go.dev/github.com/golang/mock/gomock#pkg-index
有关于 github.com/golang/mock
的使用,官方给出了一些例子,可以参考 https://github.com/golang/mock/tree/main/sample
总结
其实通常来说数据逻辑层的测试反而不容易出现问题,原因是:我们 mock 的数据都是我们想要的数据。
所以对于严格的单元测试来说,需要多组数据的测试来保证我们在一些特殊场景上能正常运行,或者满足期望运行。