后面咱们实现了最麻烦的数据层的单元测试,明天咱们来看看单元测试中最容易做的一层,数据逻辑层,也就是咱们通常说的 service
或者 biz
等,是形容具体业务逻辑的中央,这一层蕴含咱们业务最重要的逻辑。
所以它的测试十分重要,通常它测试的通过就意味着你的业务逻辑能失常运行了。
而如何对它做单元测试呢? 因为,这一层的依赖次要来源于数据层,通常这一层会调用数据层的接口来获取或操作数据。 因为咱们之前对于数据层曾经做了单元测试,所以这一次,咱们须要 mock 的不是数据库了,而是数据层。
Golang 提供了 github.com/golang/mock 来实现 mock 接口的操作,本文就是应用它来实现咱们的单元测试。
筹备工作
装置 go install github.com/golang/mock/mockgen@v1.6.0
根本 case 代码
首先咱们还是基于上一次的例子,这里给出上一次例子中所用到的接口
package serviceimport ( "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 接口
mockgen -source=./service/user.go -destination=./mock/user_mock.go -package=mock
参数名称都很好了解,我这边不赘述了。命令执行实现之后,会在 destination
生成对于的 mock 接口,就能够应用了。
生成的代码大抵如上面的样子,能够简略瞄一眼:
// Code generated by MockGen. DO NOT EDIT.// Source: ./user.go// Package mock is a generated GoMock package.package mockimport ( context "context" entity "go-demo/m/unit-test/entity" reflect "reflect" gomock "github.com/golang/mock/gomock")// MockUserRepo is a mock of UserRepo interface.type MockUserRepo struct { ctrl *gomock.Controller recorder *MockUserRepoMockRecorder}// MockUserRepoMockRecorder is the mock recorder for MockUserRepo.type MockUserRepoMockRecorder struct { mock *MockUserRepo}// NewMockUserRepo creates a new mock instance.func NewMockUserRepo(ctrl *gomock.Controller) *MockUserRepo { mock := &MockUserRepo{ctrl: ctrl} mock.recorder = &MockUserRepoMockRecorder{mock} return mock}// EXPECT returns an object that allows the caller to indicate expected use.func (m *MockUserRepo) EXPECT() *MockUserRepoMockRecorder { return m.recorder}// AddUser mocks base method.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}// AddUser indicates an expected call of AddUser.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)}// DelUser mocks base method.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}// DelUser indicates an expected call of DelUser.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)}// GetUser mocks base method.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}// GetUser indicates an expected call of GetUser.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 的接口对应须要的返回值就能够了。咱们间接来看例子:
package serviceimport ( "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"} // 无论对 AddUser 办法输出任意参数,均会返回 userInfo 信息 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) // 只有当对于 GetUser 传入 userID 为 1 时才会返回 user 信息 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 的接口上方退出正文(留神这里写的门路要和理论门路相符合):
//go:generate mockgen -source=./user.go -destination=../mock/user_mock.go -package=mocktype 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)}
而后只须要应用命令
go generate ./...
就能够生成全副的 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 的数据都是咱们想要的数据。
所以对于严格的单元测试来说,须要多组数据的测试来保障咱们在一些非凡场景上能失常运行,或者满足冀望运行。