☰
Kitex internal/mocks 完全指南:gomock 批量生成机制与单元测试实战
2026/10/14 3:08:21 网站建设 项目流程
  • 后端
  • RPC框架
  • 微服务

【免费下载链接】kitex

Go 微服务 RPC 框架,具有高性能、强可扩展的特点。

项目地址:https://gitcode.com/CloudWeGo/kitex
点击查看免费下载

Kitex 在internal/mocks目录下维护了一套覆盖框架核心接口的 Mock 代码,配合 gomock 在单元测试中隔离网络、限流、服务发现等外部依赖。本文以 internal/mocks/README.md 为骨架,结合 update.sh 的完整实现与仓库内大量测试用例,讲解如何安装工具链、扩展生成清单、一键重新生成,以及如何在 Kitex 源码测试中正确使用这些 Mock。

一、internal/mocks:Kitex 自测体系的"基础设施"

从 doc.go 的包注释可以看出,internal/mocks的定位非常明确——Package mocks contains mocks of interfaces.(该包包含各接口的 mock 实现)。它处于internal目录下,意味着这套 Mock 仅服务于 Kitex 仓库自身的测试,不对外暴露为公共 API。

整个目录由三类代码构成:

  1. gomock 自动生成:由 mockgen 从pkg/下的接口文件批量生成,文件头部带有// Code generated by MockGen. DO NOT EDIT.与// Source: ...标记,例如 limiter/limiter.go、remote/trans_handler.go。
  2. Kitex 代码生成:由 kitex/thriftgo 工具从 IDL 生成的 mock 服务,例如 thrift/ 下的test.go、stream.go,以及 proto/kitex_gen/pbapi/mock/mock.go。
  3. 手写实现:结构体 + 函数字段风格的轻量 Mock,例如 conn.go 中的Conn(实现net.Conn)、serviceinfo.go 中的MockService,以及 transhandlerclient.go、transhandlerserver.go。

它们共同支撑着 client、pkg/remote、pkg/limiter 等数十个测试文件的运行。

二、运行前提(Running Prerequisites)

1. 安装 gomock / mockgen

go install github.com/golang/mock/mockgen@latest

该命令会将mockgen安装到$GOPATH/bin(或GOBIN指定的目录)下,是 update.sh 中批量生成命令所依赖的可执行文件。安装完成后建议确认mockgen已在PATH中,可通过mockgen --version验证。

注意:gomock 生成的代码依赖github.com/golang/mock/gomock运行时包,Kitex 的 go.mod 已引入该依赖,测试代码中直接 import 即可。

2. 克隆 netpoll 到 kitex 的父目录

git clone github.com/cloudwego/netpoll

这一步是目录布局前提:update.sh的生成清单中有一条目标指向../../../netpoll/connection.go(见下文),即从internal/mocks向上三级进入 kitex 仓库的父目录去读取 netpoll 的接口定义。因此 netpoll 必须与 kitex 仓库平级存放,否则sh update.sh会在文件存在性检查处直接报错退出。

三、update.sh:批量生成 gomock 的核心脚本

update.sh 是整个 mock 生成流程的枢纽。其执行逻辑为:

  1. cd到脚本自身所在目录(internal/mocks),保证相对路径稳定;
  2. 遍历files数组,每三项为一组(source => output => package);
  3. 检查源文件是否存在,不存在则打印ERROR: ... not existing并退出;
  4. 执行mockgen -source=$infile -package=$package > $outfile;
  5. 用sed在生成文件头部插入 Apache 2.0 版权注释。

脚本中按OSTYPE区分了 macOS(darwin)与 Linux 的sed -i语法,且两个分支的版权头年份分别为 2022 与 2021——这是历史遗留差异,不影响功能。

完整的 files 生成清单

files数组以源文件路径 输出文件路径 包名三元组组织,当前仓库中完整的清单如下:

源文件(Source)输出文件(Output)包名(Package)
../../pkg/limiter/limiter.golimiter/limiter.golimiter
../../pkg/stats/tracer.gostats/tracer.gostats
../../pkg/remote/bytebuf.goremote/bytebuf.goremote
../../pkg/remote/trans_handler.goremote/trans_handler.goremote
../../pkg/remote/codec.goremote/codec.goremote
../../pkg/remote/remotecli/conn_wrapper.goremote/conn_wrapper.goremote
../../pkg/remote/connpool.goremote/connpool.goremote
../../pkg/remote/dialer.goremote/dialer.goremote
../../pkg/remote/trans_meta.goremote/trans_meta.goremote
../../pkg/remote/trans_pipeline.goremote/trans_pipeline.goremote
../../pkg/remote/payload_codec.goremote/payload_codec.goremote
../../pkg/generic/generic_service.gogeneric/generic_service.gogeneric
../../pkg/klog/log.goklog/log.goklog
../../internal/generic/thrift/thrift.gogeneric/thrift.gogeneric
../../pkg/discovery/discovery.godiscovery/discovery.godiscovery
../../pkg/loadbalance/loadbalancer.goloadbalance/loadbalancer.goloadbalance
../../pkg/proxy/proxy.goproxy/proxy.goproxy
../../pkg/utils/sharedticker.goutils/sharedticker.goutils
../../../netpoll/connection.gonetpoll/connection.gonetpoll
$GOROOT/src/net/net.gonet/net.gonet

$GOROOT/src/net/net.go这一项说明:mockgen 也支持直接对 Go 标准库源文件生成,这里用它为net标准库接口生成 mock,输出到 net/net.go。

清单之外的 rpc_info/timeout_provider.go(Source 标记为github.com/cloudwego/kitex/pkg/rpcinfo (interfaces: TimeoutProvider))与 diagnosis/service.go(Source 为.../pkg/diagnosis (interfaces: Service))则是通过 mockgen 的-interfaces参数按接口精确生成的,未出现在当前files数组中。

四、用户指南:如何为新的接口添加 mock

按 README 的用户指南,为一个新接口生成 mock 只需五步:

  1. 在files参数下新增一行,即编辑 update.sh,在数组末尾追加一条记录;
  2. 填写待 mock 接口所在的 go 文件路径(即 Source,支持仓库内相对路径、$GOROOT变量或外部仓库路径);
  3. 填写输出 mock 文件的路径(即 Output,internal/mocks下的相对路径);
  4. 填写输出 mock 文件的包名(即 Package,决定生成的// Package xxx声明与目录归属);
  5. 运行sh update.sh重新生成全部 mock 文件。

以"为pkg/limiter/limiter.go生成 mock"这一真实条目为例,它在数组中对应:

../../pkg/limiter/limiter.go limiter/limiter.go limiter

执行sh update.sh后,mockgen 会解析 pkg/limiter/limiter.go 中定义的ConcurrencyLimiter、RateLimiter、Updatable、LimitReporter四个接口,生成 limiter/limiter.go,其中包含NewMockConcurrencyLimiter(ctrl)、NewMockRateLimiter(ctrl)、NewMockUpdatable(ctrl)、NewMockLimitReporter(ctrl)四个构造函数及对应的 Recorder。

脚本本身带有两处健壮性保障:源文件缺失时立即exit 1中断;mockgen执行失败时同样exit 1,避免生成残缺文件后继续污染输出目录。

mockgen 生成代码的结构

从 limiter/limiter.go 可以看出每个生成的 mock 都遵循统一模板:

  • MockXxx结构体:持有ctrl *gomock.Controller与recorder *MockXxxMockRecorder;
  • NewMockXxx(ctrl)构造函数:接收*gomock.Controller;
  • EXPECT()方法:返回 recorder,用于声明期望调用;
  • 每个接口方法对应一对方法名/方法名(recorder):前者记录调用并返回模拟值,后者通过RecordCallWithMethodType声明期望。

生成文件的头部注释// Code generated by MockGen. DO NOT EDIT.明确告知:这类文件不应手工修改,修改应回写到源接口文件后重新运行脚本。

五、生成的 mock 全景盘点

5.1 gomock 生成(按模块)

模块目录覆盖的核心接口典型 Mock
limiter/并发/速率限流、动态更新、过载上报MockConcurrencyLimiter、MockRateLimiter、MockUpdatable、MockLimitReporter
remote/传输层编解码、读写、连接池、拨号、元信息、流水线MockClientTransHandler、MockServerTransHandler、MockTransHandler、MockDialer、MockTransReadWriter、MockGracefulShutdown、MockInvokeHandleFuncSetter等
netpoll/netpoll 连接、读写器、拨号器、监听器MockConnection、MockConn、MockListener、MockDialer,以及 nocopy.go 中的MockReader、MockWriter、MockReadWriter
discovery/服务发现MockDiscovery等
loadbalance/负载均衡MockLoadbalancer等
klog/日志接口(894 行,覆盖面最大)MockLogger等
proxy/代理转发MockProxy等
stats/链路统计追踪MockTracer
utils/共享定时任务MockTickerTask
diagnosis/诊断服务MockService
rpc_info/超时提供者MockTimeoutProvider
generic/泛化调用服务MockGenericService等
net/Go 标准库net包MockConn等

5.2 Kitex 代码生成(IDL 驱动)

  • Thrift 方向:thrift/gen.sh 使用kitex -thrift no_default_serdes -module github.com/cloudwego/kitex -gen-path ..对 test.thrift 与 stream.thrift 生成 mock 服务代码,生成后清理无用的mock与testservice目录。
  • Protobuf 方向:proto/kitex_gen/pbapi/mock/mock.go 由 Kitex v0.9.1 生成,包含UnaryTest、ClientStreamingTest、ServerStreamingTest、BidirectionalStreamingTest四种方法,覆盖了 unary 与三种流式调用模式,并提供了服务端与客户端两侧的ServiceInfo(NewServiceInfo、NewServiceInfoForClient、NewServiceInfoForStreamClient),可用于构建流式测试服务。

5.3 手写 mock(函数注入风格)

  • conn.go:Conn结构体为net.Conn的每个方法暴露一个XxxFunc函数字段,未设置的函数返回零值,并通过NewIOConn()提供一个基于bytes.Buffer的内存读写连接;文件开头var _ net.Conn = &Conn{}是编译期接口实现断言。
  • serviceinfo.go:定义了MockServiceName、MockMethod、MockExceptionMethod、MockErrorMethod、MockOnewayMethod、MockStreamingMethod等常量,以及ServiceInfo()/Service2Info()/Service3Info()三个服务信息工厂和MyService接口、MockFuncHandler函数式 handler,供服务端测试注册使用。
  • transhandlerclient.go、transhandlerserver.go:分别提供客户端/服务端传输处理器的工厂与 mock 实现,如NewMockCliTransHandlerFactory、MockSvrTransHandler、MockTransServer等。

六、在测试中使用这些 mock 的四种典型姿势

姿势一:gomock 标准流程(限流器场景)

limiter_inbound_test.go 展示了最典型的用法——先创建 Controller,再构造 mock 并声明期望:

ctrl := gomock.NewController(t) defer ctrl.Finish() concurrencyLimiter := mockslimiter.NewMockConcurrencyLimiter(ctrl) rateLimiter := mockslimiter.NewMockRateLimiter(ctrl) limitReporter := mockslimiter.NewMockLimitReporter(ctrl)

随后通过EXPECT()链式声明调用期望,例如:

concurrencyLimiter.EXPECT().Acquire(gomock.Any()).Return(true).Times(1) concurrencyLimiter.EXPECT().Release(gomock.Any()).Times(1)

这样被测的limiter中间件就能在完全不依赖真实限流实现的前提下验证 acquire/release 的配对逻辑。

姿势二:多模块 mock 组合(连接池场景)

long_pool_test.go 演示了如何将 netpoll 与 remote 两个模块的 mock 组合使用:

ctrl := gomock.NewController(t) d := mocksremote.NewMockDialer(ctrl) conn := mocksnetpoll.NewMockConnection(ctrl)

MockDialer的DialConnection期望返回MockConnection,从而让长连接池在"拨号-取连接-归还"的完整流程中脱离真实网络。类似的组合方式也大量出现在 default_client_handler_test.go 等传输层测试中。

姿势三:手写 mock(连接对象场景)

当被测代码只需要一个"能通过接口编译检查、方法调用返回零值"的连接时,直接使用手写的mocks.Conn更轻量:

conn := &mocks.Conn{} ctx, err = handler.Write(ctx, conn, msg) test.Assert(t, err == nil, err)

见 default_client_handler_test.go。这种风格适合不需要断言调用次数、只需占位的场景。

姿势四:mock 服务信息(服务端场景)

servicesearcher.go 将mocks.ServiceInfo()与mocks.MockServiceName等常量组合,构造出NewDefaultSvcSearcher():

svcInfo := mocks.ServiceInfo() s := map[string]*serviceinfo.ServiceInfo{ mocks.MockServiceName: svcInfo }

MockSvcSearcher实现了按服务名/方法名检索ServiceInfo的逻辑,供服务端路由测试使用;mock、mockException、mockError、mockOneway、mockStreaming等方法覆盖了普通、异常、错误、单向、流式多种调用形态。

七、实践建议与注意事项

  1. 两类 mock 按需选择:需要严格断言"某个接口方法被调用了几次、以什么参数调用、返回什么"时,用 gomock 生成的MockXxx+EXPECT();只需要一个"方法调用不 panic、返回零值"的占位实现时,优先用手写 mock(如mocks.Conn),代码更简洁。
  2. 生成即检查:运行sh update.sh后,建议立即在仓库根目录执行go build ./...,确认新生成的 mock 与接口签名一致。若改动过源接口,mockgen 生成的代码可能与新签名不匹配,应重新生成而非手工修改DO NOT EDIT文件。
  3. 目录布局约束:update.sh依赖../../../netpoll/connection.go与$GOROOT/src/net/net.go,前者要求 netpoll 与 kitex 平级存放,后者要求本地具备完整 Go 工具链源码树(标准 Go 安装默认满足)。
  4. 新增接口的完整流程回顾:在 update.sh 的files数组追加一行(源文件、输出文件、包名)→ 执行sh update.sh→ 在测试中import对应包并使用NewMockXxx(ctrl)。README 中给出的这五步是整个流程的可执行规范,与脚本实现一一对应。
  • 后端
  • RPC框架
  • 微服务

【免费下载链接】kitex

Go 微服务 RPC 框架,具有高性能、强可扩展的特点。

项目地址:https://gitcode.com/CloudWeGo/kitex
点击查看免费下载

相关推荐

上一篇:一张分类表 + 31 天计划:剑指 Offer 刷题路线图完整指南
下一篇:Haystack Tools 工具系统完全指南:从 @tool 装饰器到多智能体 AgentTool 的实战与源码解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询