Go 单元测试之mock接口测试

news2024/12/23 5:37:42

文章目录

    • 一、gomock 工具介绍
    • 二、安装
    • 三、使用
        • 3.1 指定三个参数
        • 3.2 使用命令为接口生成 mock 实现
        • 3.3 使用make 命令封装处理mock
    • 四、接口单元测试步骤
    • 三、小黄书Service层单元测试
    • 四、flags
    • 五、打桩(stub)
        • 参数
    • 六、总结
      • 6.1 测试用例定义
      • 6.2 设计测试用例
      • 6.3 执行测试用例代码
      • 6.4 运行测试用例
      • 6.5 不是所有的场景都很好测试

一、gomock 工具介绍

gomock 是一个 Go 语言的测试框架,在实际项目中,需要进行单元测试的时候。却往往发现有一大堆依赖项。这时候就是 Gomock 大显身手的时候了,用于编写单元测试时模拟和测试依赖于外部服务的代码。它允许你创建模拟对象(Mock Objects),这些对象可以预设期望的行为,以便在测试时模拟外部依赖,通常使用它对代码中的那些接口类型进行mock。

原本 Go 团队提供了一个 mock 工具 https://github.com/golang/mock,但在今年放弃维护了,改用 https://github.com/uber-go/mock

二、安装

要安装 gomock,你可以使用 Go 包管理器 go get

go install go.uber.org/mock/mockgen@latest

三、使用

首先确保你已经安装了gomock ,并且在项目中执行了go mod tidy

3.1 指定三个参数

在使用 mockgen 生成模拟对象(Mock Objects)时,通常需要指定三个主要参数:

  • source:这是你想要生成模拟对象的接口定义所在的文件路径。
  • destination:这是你想要生成模拟对象代码的目标路径。
  • package:这是生成代码的包名。
3.2 使用命令为接口生成 mock 实现

一旦你指定了上述参数,mockgen 就会为你提供的接口生成模拟实现。生成的模拟实现将包含一个 EXPECT 方法,用于设置预期的行为,以及一些方法实现,这些实现将返回默认值或调用真实的实现。

例如,如果你的接口定义在 ./webook/internal/service/user.go 文件中,你可以使用以下命令来生成模拟对象:

mockgen -source=./webook/internal/service/user.go -package=svcmocks destination=./webook/internal/service/mocks/user.mock.go
3.3 使用make 命令封装处理mock

在实际项目中,你可能会使用 make 命令来自动化构建过程,包括生成模拟对象。你可以创建一个 Makefilemake.bash 文件,并添加一个目标来处理 mockgen 的调用。例如:

# Makefile 示例
# mock 目标 ,可以直接使用 make mock命令
.PHONY: mock
# 生成模拟对象
mock:
	@mockgen -source=internal/service/user.go -package=svcmocks -destination=internal/service/mocks/user.mock.go
	@mockgen -package=redismocks -destination=internal/repository/cache/redismocks/cmdable.mock.go github.com/redis/go-redis/v9 Cmdable
	@go mod tidy

最后,只要我们执行make mock 命令,就会生成mock文件。

四、接口单元测试步骤

  1. 想清楚整体逻辑
  2. 定义想要(模拟)依赖项的interface(接口)
  3. 使用mockgen命令对所需mock的interface生成mock文件
  4. 编写单元测试的逻辑,在测试中使用mock
  5. 进行单元测试的验证

三、小黄书Service层单元测试

这里我们已注册接口为例子,代码如下:

// gmock/webook/backend/internal/web/user.go
func (u *UserHandler) SignUp(ctx *gin.Context) {
	type SignUpReq struct {
		Email           string `json:"email"`
		ConfirmPassword string `json:"confirmPassword"`
		Password        string `json:"password"`
	}

	var req SignUpReq
	// Bind 方法会根据 Content-Type 来解析你的数据到 req 里面
	// 解析错了,就会直接写回一个 400 的错误
	if err := ctx.Bind(&req); err != nil {
		return
	}

	ok, err := u.emailExp.MatchString(req.Email)
	if err != nil {
		ctx.String(http.StatusOK, "系统错误")
		return
	}
	if !ok {
		ctx.String(http.StatusOK, "你的邮箱格式不对")
		return
	}
	if req.ConfirmPassword != req.Password {
		ctx.String(http.StatusOK, "两次输入的密码不一致")
		return
	}
	ok, err = u.passwordExp.MatchString(req.Password)
	if err != nil {
		// 记录日志
		ctx.String(http.StatusOK, "系统错误")
		return
	}
	if !ok {
		ctx.String(http.StatusOK, "密码必须大于8位,包含数字、特殊字符")
		return
	}

	// 调用一下 svc 的方法
	err = u.svc.SignUp(ctx, domain.User{
		Email:    req.Email,
		Password: req.Password,
	})
	if err == service.ErrUserDuplicateEmail {
		ctx.String(http.StatusOK, "邮箱冲突")
		return
	}
	if err != nil {
		ctx.String(http.StatusOK, "系统异常")
		return
	}

	ctx.String(http.StatusOK, "注册成功")
}

执行命令,生成mock文件:

mockgen -source=./webook/internal/service/user.go -package=svcmocks destination=./webook/internal/service/mocks/user.mock.go

接着我们编写单元测试,代码如下:

// gmock/webook/backend/internal/web/user_test.go
package web

import (
	"bytes"
	"context"
	"errors"
	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
	"golang.org/x/crypto/bcrypt"
	"net/http"
	"net/http/httptest"
	"testing"
	"webook/internal/domain"
	"webook/internal/service"
	svcmocks "webook/internal/service/mocks"
)

func TestEncrypt(t *testing.T) {
	_ = NewUserHandler(nil, nil)
	password := "hello#world123"
	encrypted, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		t.Fatal(err)
	}
	err = bcrypt.CompareHashAndPassword(encrypted, []byte(password))
	assert.NoError(t, err)
}

func TestNil(t *testing.T) {
	testTypeAssert(nil)
}

func testTypeAssert(c any) {
	_, ok := c.(*UserClaims)
	println(ok)
}

func TestUserHandler_SignUp(t *testing.T) {
	testCases := []struct {
		name string

		mock func(ctrl *gomock.Controller) service.UserService

		reqBody string

		wantCode int
		wantBody string
	}{
		{
			name: "注册成功",
			mock: func(ctrl *gomock.Controller) service.UserService {
				usersvc := svcmocks.NewMockUserService(ctrl)
				usersvc.EXPECT().SignUp(gomock.Any(), domain.User{
					Email:    "123@qq.com",
					Password: "hello#world123",
				}).Return(nil)
				// 注册成功是 return nil
				return usersvc
			},

			reqBody: `
{
	"email": "123@qq.com",
	"password": "hello#world123",
	"confirmPassword": "hello#world123"
}
`,
			wantCode: http.StatusOK,
			wantBody: "注册成功",
		},
		{
			name: "参数不对,bind 失败",
			mock: func(ctrl *gomock.Controller) service.UserService {
				usersvc := svcmocks.NewMockUserService(ctrl)
				// 注册成功是 return nil
				return usersvc
			},

			reqBody: `
{
	"email": "123@qq.com",
	"password": "hello#world123"
`,
			wantCode: http.StatusBadRequest,
		},
		{
			name: "邮箱格式不对",
			mock: func(ctrl *gomock.Controller) service.UserService {
				usersvc := svcmocks.NewMockUserService(ctrl)
				return usersvc
			},

			reqBody: `
{
	"email": "123@q",
	"password": "hello#world123",
	"confirmPassword": "hello#world123"
}
`,
			wantCode: http.StatusOK,
			wantBody: "你的邮箱格式不对",
		},
		{
			name: "两次输入密码不匹配",
			mock: func(ctrl *gomock.Controller) service.UserService {
				usersvc := svcmocks.NewMockUserService(ctrl)
				return usersvc
			},

			reqBody: `
{
	"email": "123@qq.com",
	"password": "hello#world1234",
	"confirmPassword": "hello#world123"
}
`,
			wantCode: http.StatusOK,
			wantBody: "两次输入的密码不一致",
		},
		{
			name: "密码格式不对",
			mock: func(ctrl *gomock.Controller) service.UserService {
				usersvc := svcmocks.NewMockUserService(ctrl)
				return usersvc
			},
			reqBody: `
{
	"email": "123@qq.com",
	"password": "hello123",
	"confirmPassword": "hello123"
}
`,
			wantCode: http.StatusOK,
			wantBody: "密码必须大于8位,包含数字、特殊字符",
		},
		{
			name: "邮箱冲突",
			mock: func(ctrl *gomock.Controller) service.UserService {
				usersvc := svcmocks.NewMockUserService(ctrl)
				usersvc.EXPECT().SignUp(gomock.Any(), domain.User{
					Email:    "123@qq.com",
					Password: "hello#world123",
				}).Return(service.ErrUserDuplicateEmail)
				// 注册成功是 return nil
				return usersvc
			},

			reqBody: `
{
	"email": "123@qq.com",
	"password": "hello#world123",
	"confirmPassword": "hello#world123"
}
`,
			wantCode: http.StatusOK,
			wantBody: "邮箱冲突",
		},
		{
			name: "系统异常",
			mock: func(ctrl *gomock.Controller) service.UserService {
				usersvc := svcmocks.NewMockUserService(ctrl)
				usersvc.EXPECT().SignUp(gomock.Any(), domain.User{
					Email:    "123@qq.com",
					Password: "hello#world123",
				}).Return(errors.New("随便一个 error"))
				// 注册成功是 return nil
				return usersvc
			},

			reqBody: `
{
	"email": "123@qq.com",
	"password": "hello#world123",
	"confirmPassword": "hello#world123"
}
`,
			wantCode: http.StatusOK,
			wantBody: "系统异常",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			server := gin.Default()
			// 用不上 codeSvc
			h := NewUserHandler(tc.mock(ctrl), nil)
			h.RegisterRoutes(server)

			req, err := http.NewRequest(http.MethodPost,
				"/users/signup", bytes.NewBuffer([]byte(tc.reqBody)))
			require.NoError(t, err)
			// 数据是 JSON 格式
			req.Header.Set("Content-Type", "application/json")
			// 这里你就可以继续使用 req

			resp := httptest.NewRecorder()
			// 这就是 HTTP 请求进去 GIN 框架的入口。
			// 当你这样调用的时候,GIN 就会处理这个请求
			// 响应写回到 resp 里
			server.ServeHTTP(resp, req)

			assert.Equal(t, tc.wantCode, resp.Code)
			assert.Equal(t, tc.wantBody, resp.Body.String())

		})
	}
}

func TestMock(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	usersvc := svcmocks.NewMockUserService(ctrl)

	usersvc.EXPECT().SignUp(gomock.Any(), gomock.Any()).
		Return(errors.New("mock error"))

	//usersvc.EXPECT().SignUp(gomock.Any(), domain.User{
	//	Email: "124@qq.com",
	//}).Return(errors.New("mock error"))

	err := usersvc.SignUp(context.Background(), domain.User{
		Email: "123@qq.com",
	})
	t.Log(err)
}

四、flags

gomock 有一些命令行标志,可以帮助你控制生成过程。这些标志通常在 gomock 工具的帮助下使用,例如 gomock generate

mockgen 命令用来为给定一个包含要mock的接口的Go源文件,生成mock类源代码。它支持以下标志:

  • -source:包含要mock的接口的文件。
  • -destination:生成的源代码写入的文件。如果不设置此项,代码将打印到标准输出。
  • -package:用于生成的模拟类源代码的包名。如果不设置此项包名默认在原包名前添加mock_前缀。
  • -imports:在生成的源代码中使用的显式导入列表。值为foo=bar/baz形式的逗号分隔的元素列表,其中bar/baz是要导入的包,foo是要在生成的源代码中用于包的标识符。
  • -aux_files:需要参考以解决的附加文件列表,例如在不同文件中定义的嵌入式接口。指定的值应为foo=bar/baz.go形式的以逗号分隔的元素列表,其中bar/baz.go是源文件,foo是-source文件使用的文件的包名。
  • -build_flags:(仅反射模式)一字不差地传递标志给go build
  • -mock_names:生成的模拟的自定义名称列表。这被指定为一个逗号分隔的元素列表,形式为Repository = MockSensorRepository,Endpoint=MockSensorEndpoint,其中Repository是接口名称,mockSensorrepository是所需的mock名称(mock工厂方法和mock记录器将以mock命名)。如果其中一个接口没有指定自定义名称,则将使用默认命名约定。
  • -self_package:生成的代码的完整包导入路径。使用此flag的目的是通过尝试包含自己的包来防止生成代码中的循环导入。如果mock的包被设置为它的一个输入(通常是主输入),并且输出是stdio,那么mockgen就无法检测到最终的输出包,这种情况就会发生。设置此标志将告诉 mockgen 排除哪个导入
  • -copyright_file:用于将版权标头添加到生成的源代码中的版权文件
  • -debug_parser:仅打印解析器结果
  • -exec_only:(反射模式) 如果设置,则执行此反射程序
  • -prog_only:(反射模式)只生成反射程序;将其写入标准输出并退出。
  • -write_package_comment:如果为true,则写入包文档注释 (godoc)。(默认为true)

五、打桩(stub)

在测试中,打桩是一种测试术语,用于为函数或方法设置一个预设的返回值,而不是调用真实的实现。在 gomock 中,打桩通常通过设置期望的行为来实现。
例如,您可以为 myServiceMockDoSomething 方法设置一个期望的行为,并返回一个特定的错误。这可以通过调用 myServiceMock.EXPECT().DoSomething().Return(error) 来实现。
在单元测试中,使用 gomock 可以帮助你更有效地模拟外部依赖,从而编写更可靠和更高效的测试。通常用来屏蔽或补齐业务逻辑中的关键代码方便进行单元测试。

屏蔽:不想在单元测试用引入数据库连接等重资源

补齐:依赖的上下游函数或方法还未实现

gomock支持针对参数、返回值、调用次数、调用顺序等进行打桩操作。

参数

参数相关的用法有:

  • gomock.Eq(value):表示一个等价于value值的参数
  • gomock.Not(value):表示一个非value值的参数
  • gomock.Any():表示任意值的参数
  • gomock.Nil():表示空值的参数
  • SetArg(n, value):设置第n(从0开始)个参数的值,通常用于指针参数或切片

六、总结

6.1 测试用例定义

测试用例定义,最完整的情况下应该包含:

  • 名字:简明扼要说清楚你测试的场景,建议用中文。
  • 预期输入:也就是作为你方法的输入。如果测试的是定义在类型上的方法,那么也可以包含类型实例。
  • 预期输出:你的方法执行完毕之后,预期返回的数据。如果方法是定义在类型上的方法,那么也可以包含执行之后的实例的状态。
  • mock:每一个测试需要使用到的mock状态。单元测试里面常见,集成测试一般没有。
  • 数据准备:每一个测试用例需要的数据。集成测试里常见。
  • 数据清理:每一个测试用例在执行完毕之后,需要执行一些数据清理动作。集成测试里常见。

如果你要测试的方法很简单,那么你用不上全部字段。

6.2 设计测试用例

测试用例定义和运行测试用例都是很模板化的东西。测试用例就是要根据具体的方法来设计。

  • 如果是单元测试:看代码,最起码做到分支覆盖。
  • 如果是集成测试:至少测完业务层面的主要正常流程和主要异常流程。

单元测试覆盖率做到80%以上,在这个要求之下,只有极少数的异常分支没有测试。其它测试就不是我们研发要考虑的了,让测试团队去搞。

6.3 执行测试用例代码

测试用例定义出来之后,怎么执行这些用例,就已经呼之欲出了。

这里分成几个部分:

  • 初始化 mock 控制器,每个测试用例都有独立的 mock 控制器。
  • 使用控制器 ctrl 调用 tc.mock,拿到 mock 的 UserService 和 CodeService。
  • 使用 mock 的服务初始化 UserHandler,并且注册路由。
  • 构造 HTTP 请求和响应 Recorder
  • 发起调用 ServeHTTP

6.4 运行测试用例

测试里面的testCases是一个匿名结构体的切片,所以运行的时候就是直接遍历。

那么针对每一个测试用例:

  • 首先调用mock部分,或者执行before。
  • 执行测试的方法。
  • 比较预期结果。
  • 调用after方法。

注意运行的时候,先调用了t.Run,并且传入了测试用例的名字。

6.5 不是所有的场景都很好测试

**即便你的代码写得非常好,但是有一些场景基本上不可能测试到。**如图中的error分支,就是属于很难测试的。

因为bcrypt包你控制不住,Generate这个方法只有在超时的时候才会返回error。那么你不测试也是可以的,代码review可以确保这边正确处理了error

记住:没有测试到的代码,一定要认真review

小黄书单元测试代码:https://github.com/tao-xiaoxin/demo/tree/main/gotest/gmock/webook/backend

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1606305.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

医学图像三维重建与可视化系统 医学图像分割 区域增长

医学图像的三维重建与可视化,这是一个非常有趣且具有挑战性的课题!在这样的项目中,可以探索不同的医学图像技术,比如MRI、CT扫描等,然后利用这些图像数据进行三维重建,并将其可视化以供医生或研究人员使用。…

解决Mac使用Vscode无法调用外部终端

前言 今天遇到一个很奇怪的问题,之前好好的用Vscode还能调用外部终端,怎么今天不行了?问题出在哪里呢?请听我娓娓道来。 检查配置文件 我查看了一下配置文件,发现配置文件都是调用外部控制台,没毛病啊。 …

Objective-C网络数据捕获:使用MWFeedParser库下载Stack Overflow示例

概述 Objective-C开发中,网络数据捕获是一项常见而关键的任务,特别是在处理像RSS源这样的实时网络数据流时。MWFeedParser库作为一个优秀的解析工具,提供了简洁而强大的解决方案。本文将深入介绍如何利用MWFeedParser库,以高效、…

(最详细)关于List和Set的区别与应用

关于List与Set的区别 List和Set都继承自Collection接口; List接口的实现类有三个:LinkedList、ArrayList、Vector。Set接口的实现类有两个:HashSet(底层由HashMap实现)、LinkedHashSet。 在List中,List.add()是基于数组的形式来添…

Spring Boot后端+Vue前端:打造高效二手车交易系统

作者介绍:✌️大厂全栈码农|毕设实战开发,专注于大学生项目实战开发、讲解和毕业答疑辅导。 🍅获取源码联系方式请查看文末🍅 推荐订阅精彩专栏 👇🏻 避免错过下次更新 Springboot项目精选实战案例 更多项目…

机器学习笔记 - 使用 OpenCV 的结构化森林进行边缘检测

一、简述 边缘检测是计算机视觉领域中一项非常重要的任务。这是许多纯计算机视觉任务(例如轮廓检测)的第一步。即使涉及深度学习,较深层也首先学习识别边缘,然后再学习图像的复杂特征。所以,我们可以说边缘检测在计算机视觉领域非常重要。拥有良好且高效的图像边缘检测算法…

后端返回的数据中含有Null的则不在前端展示

方式 1:application 上加配置 只需要在配置文件 上,增加 如下配置。 application 格式配置: spring.jackson.default-property-inclusionnon_null yml 格式配置: spring:jackson:default-property-inclusion: non_null注意&a…

美团财务科技后端一面:如何保证数据一致性?延时双删第二次失败如何解决?

更多大厂面试内容可见 -> http://11come.cn 美团财务科技后端一面:项目内容拷打 美团财务科技后端一面:项目相关面试题,主要包含 Zset、延时双删失败重试、热点数据解决、ThreadLocal 这几个方面相关的内容 由于前几个问题是对个人项目的…

混合云构建-如何创建一个高可用的Site to Site VPN 连接 Azure 和GCP云

在现代云计算环境中,企业通常会采用多云战略,将工作负载分布在不同的云服务提供商上。这种方式可以提高可用性、降低供应商锁定风险,并利用每个云提供商的独特优势。然而,在这种情况下,需要确保不同云环境之间的互联互通,以实现无缝的数据传输和应用程序集成。 本文将详细介绍…

微信小程序之点击事件

微信小程序中常用的点击事件主要是 tap,但除此之外还有其他的触摸类事件,用于不同的交互场景。以下是一些常见的点击和触摸相关的事件及其区别: 1、tap——最基本的点击事件,适用于一般的轻触交互,类似于 HTML 中的 c…

浅析binance新币OMNI的前世今生

大盘跌跌不休,近期唯一的指望就是binance即将上线的OMNI 。虽然目前查到的空投数量不及预期,但OMNI能首发币安,确实远超预期了。 OMNI代币总量1亿,初始流通仅10,391,492枚,其中币安Lanchpool可挖350万枚 对于OMNI这个…

【做算法学数据结构】【链表】删除排序链表中的重复元素

链表 链表单向链表双向链表 题目代码 链表 当涉及到数据结构时,链表是一种常见且重要的数据结构。链表由一系列节点组成,每个节点包含数据和指向下一个节点的引用。相比于数组,链表的大小可以动态地增长或缩小,因为每个节点只需要…

风力发电自动化控制系统中的智能化技术应用研究

风力发电自动化控制系统中的智能化技术应用研究 随碳中和目标的提出和执行,风能发电作为新能源行业的核心部分,步入了它的黄金发展期。由于风能资源具有间歇性、随机性等特点,这给风电的高效利用带来了巨大挑战。为了增强风力发电系统的工作效…

枚举类型介绍

1.如果一个变量只有几种可能的值,比如星期几 son,mon,tus,wed,fri,sat 列表中的名字,可以自己定义,无需像变量一样去申请 枚举变量:只限列表中的情况,值默认从0开始,枚举元素不能被赋值,可以指定…

OpenCV从入门到精通实战(七)——探索图像处理:自定义滤波与OpenCV卷积核

本文主要介绍如何使用Python和OpenCV库通过卷积操作来应用不同的图像滤波效果。主要分为几个步骤:图像的读取与处理、自定义卷积函数的实现、不同卷积核的应用,以及结果的展示。 卷积 在图像处理中,卷积是一种重要的操作,它通过…

JAVA网络编程、项目验证码实现

什么是网络编程? 在网络通信协议下,不同计算机上运行的程序,进行的数据传输。 应用场景:即时通信、网游对战、金融证券、国际贸易、邮件、等等 不管是什么场景,都是计算机跟计算机之间通过网络进行数据传输 Java中可以使用ja…

面试经典算法系列之二叉树17 -- 验证二叉树

面试经典算法32 - 验证二叉树 LeetCode.98 公众号:阿Q技术站 问题描述 给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下: 节点的左子树只包含 小于 当前节点的数。节点的右子树只包含 大于 当…

浅写个登录(无js文件)

全部代码如下&#xff0c;无需编写wxss文件&#xff0c;渲染都在style里面&#xff1a; <view style"height: 250rpx;width: 100%;"> <!-- 背景图片 --><view style"position: absolute; background-color: antiquewhite; height: 250rpx;width…

OpenHarmony UI动画-recyclerview_animators

简介 带有添加删除动画效果以及整体动画效果的list组件库 下载安装 ohpm install ohos/recyclerview-animatorsOpenHarmony ohpm 环境配置等更多内容&#xff0c;请参考如何安装OpenHarmony ohpm 包 使用说明 引入组件库 import { RecyclerView } from "ohos/recycler…

RabbitMQ项目实战(一)

文章目录 RabbitMQ项目实战选择客户端基础实战 前情提要&#xff1a;我们了解了消息队列&#xff0c;RabbitMQ的入门&#xff0c;交换机&#xff0c;以及核心特性等知识&#xff0c;现在终于来到了激动人心的项目实战环节&#xff01;本小节主要介绍通过Spring Boot RabbitMQ S…