Golang實現(xiàn)單元測試中的邏輯層
前面我們完成了最麻煩的數(shù)據(jù)層的單元測試,今天我們來看看單元測試中最容易做的一層,數(shù)據(jù)邏輯層,也就是我們通常說的 service 或者 biz 等,是描述具體業(yè)務邏輯的地方,這一層包含我們業(yè)務最重要的邏輯。
所以它的測試非常重要,通常它測試的通過就意味著你的業(yè)務邏輯能正常運行了。
而如何對它做單元測試呢? 因為,這一層的依賴主要來源于數(shù)據(jù)層,通常這一層會調(diào)用數(shù)據(jù)層的接口來獲取或操作數(shù)據(jù)。 由于我們之前對于數(shù)據(jù)層已經(jīng)做了單元測試,所以這一次,我們需要 mock 的不是數(shù)據(jù)庫了,而是數(shù)據(jù)層。
Golang 提供了 github.com/golang/mock 來實現(xiàn) mock 接口的操作,本文就是使用它來完成我們的單元測試。
準備工作
安裝 go install github.com/golang/mock/mockgen@v1.6.0
基本 case 代碼
首先我們還是基于上一次的例子,這里給出上一次例子中所用到的接口
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 接口
mockgen -source=./service/user.go -destination=./mock/user_mock.go -package=mock
參數(shù)名稱都很好理解,我這邊不贅述了。命令執(zhí)行完成之后,會在 destination 生成對于的 mock 接口,就可以使用了。
生成的代碼大致如下面的樣子,可以簡單瞄一眼:
// 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 的單元測試編寫起來也很方便,只需要調(diào)用 EXPECT() 方法,將需要 mock 的接口對應需要的返回值就可以了。我們直接來看例子:
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 方法輸入任意參數(shù),均會返回 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 做斷言來驗證最終結(jié)果??梢钥吹?,單元測試編寫起來并不難。
優(yōu)化
當然,如果我們每次修改接口或者新增接口都需要重新執(zhí)行一次命令,一個文件還好,當有很多文件的時候肯定是非常困難的。所以我們需要使用 go:generate 來優(yōu)化一下。
我們可以在需要 mock 的接口上方加入注釋(注意這里寫的路徑要和實際路徑相符合):
//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)
}然后只需要使用命令
go generate ./...
就可以生成全部的 mock 嘞,所以及時文件很多,只需要利用好 go:generate 也能一次搞定
mockgen
比如針對指定參數(shù),我們偷懶可以都用 Any,但常常還需要用 gomock.Eq() 或 gomock.Not("Sam")有關(guān) gomock 還有很多方法在測試的使用也很有用,詳細的文檔在:https://pkg.go.dev/github.com/golang/mock/gomock#pkg-index
有關(guān)于 github.com/golang/mock 的使用,官方給出了一些例子,可以參考 https://github.com/golang/mock/tree/main/sample
總結(jié)
其實通常來說數(shù)據(jù)邏輯層的測試反而不容易出現(xiàn)問題,原因是:我們 mock 的數(shù)據(jù)都是我們想要的數(shù)據(jù)。
所以對于嚴格的單元測試來說,需要多組數(shù)據(jù)的測試來保證我們在一些特殊場景上能正常運行,或者滿足期望運行。
到此這篇關(guān)于Golang實現(xiàn)單元測試中的邏輯層的文章就介紹到這了,更多相關(guān)Golang單元測試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go實現(xiàn)thrift的網(wǎng)絡(luò)傳輸性能及需要注意問題示例解析
這篇文章主要為大家介紹了go實現(xiàn)thrift的網(wǎng)絡(luò)傳輸性能及需要注意問題示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09
golang構(gòu)建HTTP服務的實現(xiàn)步驟
其實很多框架都是在 最簡單的http服務上做擴展的的,基本上都是遵循h(huán)ttp協(xié)議,本文主要介紹了golang構(gòu)建HTTP服務,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12
使用Go語言創(chuàng)建靜態(tài)文件服務器問題
這篇文章主要介紹了使用Go語言創(chuàng)建靜態(tài)文件服務器,本文通過試了代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03
golang基礎(chǔ)之字符串與int、int64類型互相轉(zhuǎn)換
這篇文章主要給大家介紹了關(guān)于golang基礎(chǔ)之字符串與int、int64類型互相轉(zhuǎn)換的相關(guān)資料,在Go語言中string轉(zhuǎn)int是一項常見的操作,需要的朋友可以參考下2023-07-07

