Golang实现单元测试中的逻辑层

目录
  • 准备工作
  • 基本 case 代码
    • 生成 mock 接口
    • 编写单元测试
  • 优化
  • mockgen
  • 总结

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

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

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

Golang 提供了 github.com/golang/mock 来实现 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

参数名称都很好理解,我这边不赘述了。命令执行完成之后,会在 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 的单元测试编写起来也很方便,只需要调用 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 方法输入任意参数,均会返回 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=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

比如针对指定参数,我们偷懒可以都用 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 的数据都是我们想要的数据。

所以对于严格的单元测试来说,需要多组数据的测试来保证我们在一些特殊场景上能正常运行,或者满足期望运行。

到此这篇关于Golang实现单元测试中的逻辑层的文章就介绍到这了,更多相关Golang单元测试内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • GOLang单元测试用法详解

    目录 概念 go test基本用法 go test 基础用例 测试可执行程序 外部测试包解决循环依赖 测试覆盖比例 测试基准函数 概念 单元测试 UT测试,针对程序来进行正确检测测试工作,一个优秀强壮代码 需要有完美的 UT测试用例 go test基本用法 go test 测试用例放在 *_test.go 文件中,与被测函数放到同一个目录下面go build 时候不会构建成包一部分 被测试用例函数命名 TestXXX. 第一个字母必须大写 测试函数: 检测逻辑是否正确 基准函数以BenChmar

  • 使用Go进行单元测试的实现

    简介 日常开发中, 测试是不能缺少的. Go 标准库中有一个叫做 testing 的测试框架, 可以用于单元测试和性能测试. 它是和命令 go test 集成使用的. 测试文件是以后缀 _test.go 命名的, 通常和被测试的文件放在同一个包中. 单元测试 单元测试的格式形如: func TestAbs(t *testing.T) { got := Abs(-1) if got != 1 { t.Errorf("Abs(-1) = %d; want 1", got) } } 在 ut

  • Golang 单元测试和基准测试实例详解

    目录 前言 Go 单元测试 单元测试覆盖率 基准测试 前言 多人协作的项目里,要保证代码的质量,自然离不开单元测试.开发完一个功能后肯定要对所写的代码进行测试,测试没有问题之后再合并到代码库供他人使用.如果强行合并到代码库可能会影响其他人开发,被上线的话肯定也会导致线上 Bug ,影响用户使用. 所以,单元测试也是一个很重要的事情.单元测试是指在开发中,对一个函数或模块的测试.其强调的是对单元进行测试. Go 单元测试 Go 语言提供了单元测试的框架,只要遵循其规则即可: 测试文件命名: 单元测

  • Go 语言进阶单元测试示例详解

    目录 前言 测试 单元测试 规则 示例 assert 覆盖率 依赖 Mock 基准测试 前言 本文从单元测试实践角度出发,提升对代码质量的意识. 本文内容主要包括:单元测试.Mock测试.基准测试. 测试 测试可以提高代码的质量.减少事故的发生. 测试又分为:回归测试.集成测试.单元测试. 回归测试是指对QA手动回归一些特定场景,可以理解为我们说的手动点点. 集成测试是指对系统功能维度做验证,比如对服务暴露的接口验证,一般是自动化的验证. 单元测试是指在开发阶段,开发者对单独的函数.模块做验证,

  • Go单元测试工具gomonkey的使用

    目录 Go 单元测试工具 单测 Go 单元测试工具 gomonkey gomonkey 打桩失败的可能原因 goconvey 为全局变量打一个桩 为一个函数打桩 什么是内联? Go 单元测试工具 测试分为4个层次 单元测试:对代码进行测试 集成测试:对一个服务的接口测试 端到端测试(链路测试):从一个链路的入口输入测试用例,验证输出的系统的结果 UI测试 常犯的错误: 没有断言.没有断言的单测是没有灵魂的. 单测的特征: A:(Automatic,自动化):单元测试应该是全自动执行的,并且非交互

  • Golang实现单元测试中的逻辑层

    目录 准备工作 基本 case 代码 生成 mock 接口 编写单元测试 优化 mockgen 总结 前面我们完成了最麻烦的数据层的单元测试,今天我们来看看单元测试中最容易做的一层,数据逻辑层,也就是我们通常说的 service 或者 biz 等,是描述具体业务逻辑的地方,这一层包含我们业务最重要的逻辑. 所以它的测试非常重要,通常它测试的通过就意味着你的业务逻辑能正常运行了. 而如何对它做单元测试呢? 因为,这一层的依赖主要来源于数据层,通常这一层会调用数据层的接口来获取或操作数据. 由于我们

  • 在ASP.NET 2.0中操作数据之二:创建一个业务逻辑层

    导言 本教程的第一节所描述的数据访问层(Data Access Layer,以下简称为DAL)已经清晰地将表示逻辑与数据访问逻辑区分开了.不过,即使DAL将数据访问的细节从表示层中分离出来了,可它却不能处理任何的业务规则.比如说,我们可能不希望产品表中那些被标记为"停用"的产品的"分类编号"或"供应商编号"被更新:我们还可能需要应用一些资历规则,比如说我们都不希望被比自己的资历还要浅的人管理.另外一个比较常见的情况就是授权,比如说只有那些具有特殊

  • 《解剖PetShop》之五:PetShop之业务逻辑层设计

    五 PetShop之业务逻辑层设计 业务逻辑层(Business Logic Layer)无疑是系统架构中体现核心价值的部分.它的关注点主要集中在业务规则的制定.业务流程的实现等与业务需求有关的系统设计,也即是说它是与系统所应对的领域(Domain)逻辑有关,很多时候,我们也将业务逻辑层称为领域层.例如Martin Fowler在<Patterns of Enterprise Application Architecture>一书中,将整个架构分为三个主要的层:表示层.领域层和数据源层.作为领

  • ASP.NET MVC5网站开发之业务逻辑层的架构和基本功能 (四)

    业务逻辑层在Ninesky.Core中实现,主要功能封装一些方法通过调用数据存储层,向界面层提供服务. 一.业务逻辑层的架构 Ninesky.Core包含三个命名空间Ninesky.Core.Ninesky.Core.Types.Ninesky.Core.General. Ninesky.Core包含模型和功能实现,Ninesky.Core.Types是项目用到的一些类型的定义,Ninesky.Core.General是项目用到的一些方法的定义. 1.Ninesky.Core命名空间的结构 Ni

  • 微信小程序 视图层(xx.xml)和逻辑层(xx.js)详细介绍

    微信小程序可以理解为云OS的概念,微信生态本身就是一个OS.加上微信公众平台和微信开发平台本身已经是非常成熟的架构,能够完美媲美App的功能,同时在交互体验方面也能够做到极致,大有取代App之势.苹果App Store模式的意义在于为第三方软件的提供者提供了方便而又高效的一个软件销售平台.用户购买应用所支付的费用由苹果与应用开发商3:7分成.如果微信小程序商城也采用类似的分佣模式,那8亿多的用户将是一个非常大的无形资产,成为腾讯继游戏.会员.广告后的另一个掘金源. 微信小程序允许人们无需进行下载

  • 浅析Vue中拆分视图层代码的5点建议

    一.框架的定位 框架通常只是一种设计模式的实现,它并不意味着你可以在开发中避免所有分层设计工作. SPA 框架几乎都是基于 MVC 或 MVVM 设计模式而建立起来的,这些模式都只是宏观的分层设计,当代码量开始随着项目增大而增多时,问题就会越来越多.许多企业内部的项目仍然在使用 angularjs1.X ,你会发现许多 controller 的体积大到令人发指,稍有经验的团队会利用好 angularjs1 构建的 controller , service , filter 以及路由和消息机制来完

  • Golang 统计字符串中数字字母数量的实现方法

    目录 1.需求说明 2.实现 2.1 ASCII 码值法 2.2 正则表达式 3.性能对比 4.小结 参考文献 1.需求说明 记录一下项目对用户 UGC 文本进行字数限制的具体实现. 不同的产品,出于种种原因,一般都会对用户输入的文本内容做字数限制. 出于产品定位,比如 140 字符限制的 Twitter,让内容保持简洁凝练,易于阅读: 出于用户的阅读体验,过多的文字会造成阅读疲劳,合适的字数能够提高阅读舒适度: 出于技术与成本的考虑,不设上限的 UGC 内容会引发一些潜在的问题,比如增加存储的

  • SpringMVC中的表现层结果封装

    目录 一.为什么要将返回结果封装? 二.封装步骤 1.自定义封装类 2.编写 Code 类统一状态码 三.案例演示 1.需求分析 2.编写 UserController 类 3.使用 Postman 工具测试 一.为什么要将返回结果封装? 当前端发送一个请求,被处理后,我们之前的处理方式就是,对于增删改操作,我们返回 true 或 false:对于查询操作,我们就将查询后的结果返回. 虽然使用该方式能够将请求结果返回,但是前端开发人员在解析返回的数据时就会非常麻烦,他们需要根据你不同的返回结果定

  • 详解Golang 与python中的字符串反转

    详解Golang 与python中的字符串反转 在go中,需要用rune来处理,因为涉及到中文或者一些字符ASCII编码大于255的. func main() { fmt.Println(reverse("Golang python")) } func reverse(src string) string { dst := []rune(src) len := len(dst) var result []rune result = make([]rune, 0) for i := le

  • 理解SQL SERVER中的逻辑读,预读和物理读

    SQL SERVER数据存储的形式 在谈到几种不同的读取方式之前,首先要理解SQL SERVER数据存储的方式.SQL SERVER存储的最小单位为页(Page).每一页大小为8k,SQL SERVER对于页的读取是原子性,要么读完一页,要么完全不读,不会有中间状态.而页之间的数据组织结构为B树(请参考我之前的博文).所以SQL SERVER对于逻辑读,预读,和物理读的单位是页. SQL SERVER一页的总大小为:8K 但是这一页存储的数据会是:8K=8192字节-96字节(页头)-36字节(

随机推荐