前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang 单元测试 - 逻辑层

Golang 单元测试 - 逻辑层

作者头像
LinkinStar
发布2023-02-22 14:07:42
4400
发布2023-02-22 14:07:42
举报
文章被收录于专栏:LinkinStar's Blog

前面我们完成了最麻烦的数据层的单元测试,今天我们来看看单元测试中最容易做的一层,数据逻辑层,也就是我们通常说的 service 或者 biz 等,是描述具体业务逻辑的地方,这一层包含我们业务最重要的逻辑。

所以它的测试非常重要,通常它测试的通过就意味着你的业务逻辑能正常运行了。

而如何对它做单元测试呢? 因为,这一层的依赖主要来源于数据层,通常这一层会调用数据层的接口来获取或操作数据。 由于我们之前对于数据层已经做了单元测试,所以这一次,我们需要 mock 的不是数据库了,而是数据层。

Golang 提供了 github.com/golang/mock 来实现 mock 接口的操作,本文就是使用它来完成我们的单元测试。

准备工作

安装 go install github.com/golang/mock/mockgen@v1.6.0

基本 case 代码

首先我们还是基于上一次的例子,这里给出上一次例子中所用到的接口

代码语言:javascript
复制
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 接口的几个方法,就可以测试我们 AddUserGetUser 方法了

生成 mock 接口

使用 mockgen 命令可以生成我们所需要的 mock 接口

代码语言:javascript
复制
mockgen -source=./service/user.go -destination=./mock/user_mock.go -package=mock

参数名称都很好理解,我这边不赘述了。命令执行完成之后,会在 destination 生成对于的 mock 接口,就可以使用了。

生成的代码大致如下面的样子,可以简单瞄一眼:

代码语言:javascript
复制
// Code generated by MockGen. DO NOT EDIT.
// Source: ./user.go

// Package mock is a generated GoMock package.
package mock

import (
	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 的接口对应需要的返回值就可以了。我们直接来看例子:

代码语言:javascript
复制
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"}
	// 无论对 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 的接口上方加入注释(注意这里写的路径要和实际路径相符合):

代码语言:javascript
复制
//go:generate mockgen -source=./user.go -destination=../mock/user_mock.go -package=mock
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)
}

然后只需要使用命令

代码语言:javascript
复制
go generate ./...

就可以生成全部的 mock 嘞,所以及时文件很多,只需要利用好 go:generate 也能一次搞定

mockgen

比如针对指定参数,我们偷懒可以都用 Any,但常常还需要用 gomock.Eq()gomock.Not("Sam")

总结

其实通常来说数据逻辑层的测试反而不容易出现问题,原因是:我们 mock 的数据都是我们想要的数据。 所以对于严格的单元测试来说,需要多组数据的测试来保证我们在一些特殊场景上能正常运行,或者满足期望运行。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-02-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 准备工作
  • 基本 case 代码
    • 生成 mock 接口
      • 编写单元测试
      • 优化
      • mockgen
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档