☰
深入解析 mockery 多模板渲染:同一 Go 接口生成多套 Mock 的机制与实践
2026/9/29 6:18:21 网站建设 项目流程
  • 开发工具
  • 代码生成
  • 测试

【免费下载链接】mockery

A mock code autogenerator for Go

项目地址:https://gitcode.com/gh_mirrors/moc/mockery
点击查看免费下载

Mockery 的本质是一个 Go 接口代码生成框架,其核心能力之一是针对同一个接口,使用不同模板渲染出不同风格的 Mock 实现。仓库中的 internal/fixtures/multi_template/ 目录正是为验证这一能力而设计的测试夹具:它用同一个Foo接口,同时生成了matryer风格与testify风格两套 Mock,并在测试中混用它们。读完本文,你将掌握 mockery 内置模板的切换配置、两种模板产物的结构差异、模板选择背后的源码调用链,以及如何通过template配置参数扩展出自定义风格的生成代码。

1. multi_template 测试夹具的整体设计

该夹具位于 internal/fixtures/multi_template/,其 README.md 用一句话点明了存在意义:

This package tests that mockery can render different templates for the same interface.

即:验证 mockery 能够为同一个接口渲染出不同的模板实现。目录内的文件分工非常清晰:

文件角色
interface.go被 mock 的目标接口定义
mocks_matryer_multitemplate_test.gomatryer模板生成的 Mock(生成结果,已提交入库)
mocks_testify_multitemplate_test.gotestify模板生成的 Mock(生成结果,已提交入库)
interface_test.go同时使用两套 Mock 的测试用例

被 mock 的接口非常简单,只有一个方法,方便对比两套模板在同样输入下的输出差异:

package multitemplate type Foo interface { Bar() string }

2. 两种内置模板的产物对比

对同一个Foo接口,mockery 分别生成了MockMatryerFoo与MockTestifyFoo两个结构截然不同的 Mock。这正是template配置参数决定生成形态的直观证据。

2.1 matryer 模板:基于函数字段的 Mock

mocks_matryer_multitemplate_test.go 的头部注释明确标注了生成来源:// github.com/vektra/mockery与// template: matryer。该风格借鉴了 matryer/moq 项目(该项目已并入 mockery),其核心设计是用函数字段承载方法实现:

type MockMatryerFoo struct { // BarFunc mocks the Bar method. BarFunc func() string // calls tracks calls to the methods. calls struct { // Bar holds details about calls to the Bar method. Bar []struct { } } lockBar sync.RWMutex }

关键特征:

  • 直接注入函数:使用方通过给BarFunc字段赋函数来定制行为,无需任何断言机制;
  • 调用追踪:每次调用Bar()时,方法先把调用信息追加进calls.Bar,再执行BarFunc,并使用sync.RWMutex(lockBar)保护并发安全;
  • 调用记录查询:BarCalls()返回历史调用列表,可供断言使用。

生成的Bar()实现体现了"先记录、后执行"的调用链:

func (mock *MockMatryerFoo) Bar() string { if mock.BarFunc == nil { panic("MockMatryerFoo.BarFunc: method is nil but Foo.Bar was just called") } callInfo := struct{}{} mock.lockBar.Lock() mock.calls.Bar = append(mock.calls.Bar, callInfo) mock.lockBar.Unlock() return mock.BarFunc() }

2.2 testify 模板:基于 Expecter 的 Mock

mocks_testify_multitemplate_test.go 则基于 testify 生态,头部同样标注// template: testify。其核心设计是通过期望匹配(expectation matching)驱动行为:

type MockTestifyFoo struct { mock.Mock } type MockTestifyFoo_Expecter struct { mock *mock.Mock }

关键特征:

  • 构造器:NewMockTestifyFoo(t)接收*testing.T,自动注册mock.Mock.Test(t),并通过t.Cleanup在测试结束时自动执行AssertExpectations校验所有期望是否被满足;
  • Expecter 链式 API:EXPECT().Bar().Return("bar")声明期望,Run/Return/RunAndReturn提供类型安全的链式设置;
  • 返回值断言:Bar()通过_mock.Called()收集调用参数与返回值,若未配置期望会panic("no return value specified for Bar")。

2.3 同一测试中混用两套 Mock

interface_test.go 演示了两套 Mock 可以在同一个测试函数中并存、各司其职:

func TestFoo(t *testing.T) { testifyMock := NewMockTestifyFoo(t) testifyMock.EXPECT().Bar().Return("bar") assert.Equal(t, "bar", testifyMock.Bar()) matryerMock := MockMatryerFoo{ BarFunc: func() string { return "bar" }, } assert.Equal(t, "bar", matryerMock.Bar()) }

这段测试同时验证了两种风格的使用范式:testify 风格"声明期望→调用→自动断言";matryer 风格"注入函数→调用→人工校验返回值"。这也证明 mockery 生成的两套 Mock 互不干扰,可以出现在同一包内(注意:结构体名不同,因此不存在命名冲突)。

3. 模板选择背后的源码实现

为什么同一个接口能渲染出两套代码?答案在模板生成器的实现中。

3.1 内置模板的注册表

internal/template_generator.go 通过//go:embed将两个模板及其 JSON Schema 编译进二进制,并以 map 形式注册(L34-L55):

var styleTemplates = map[string]string{ "matryer": templateMatryer, "testify": templateTestify, } var jsonSchemas = map[string]string{ "matryer": templateMatryerJSONSchema, "testify": templateTestifyJSONSchema, }

模板源文件分别是 internal/mock_matryer.templ 与 internal/mock_testify.templ,两者都用 Gotext/template语法书写:开头是boilerplate-file、mock-build-tags等可选片段,随后遍历.Interfaces与每个接口的.Methods输出对应代码结构。两套模板对同一份template.Data做不同渲染,就得到了风格迥异的 Mock。

3.2 getTemplate 的解析优先级

TemplateGenerator.getTemplate(internal/template_generator.go)按以下顺序解析template配置:

  1. 以file://、https://、http://开头的值,走远程/文件模板加载(NewRemoteTemplate负责下载,带缓存);
  2. 否则在内置styleTemplatesmap 中按名字查找,找不到即报错template '<name>' does not exist。

也就是说,template: testify命中的是嵌入的templateTestify,而template: file://./my.tmpl则加载自定义模板文件。

3.3 生成主流程

Generate方法(internal/template_generator.go)完整展示了多模板渲染的流水线:

  1. 从template.Registry中LookupInterface查找接口;
  2. 对每个方法调用methodData提取参数、返回值与类型信息(含GetReplacement类型替换支持);
  3. 组装template.NewInterface,连同包级配置一起构造template.NewData;
  4. getTemplate取出目标模板字符串与 JSON Schema;
  5. validateSchema校验模板数据是否符合 Schema;
  6. templ.Execute执行模板渲染;
  7. g.format按formatter配置(gofmt/goimports/noop)对内存中的产物做格式化后输出。

这套流程对任何模板一视同仁,正是"同一接口、不同模板"得以成立的基础。

4. 如何配置与切换模板

模板选择完全由配置参数驱动。在 docs/configuration.md 的参数表中:

  • template:选择要渲染的模板。内置可选值为testify(默认)与matryer,也支持file://、https://、http://前缀指向自定义模板(见 docs/template/index.md);
  • template-data:map[string]any,向模板传入任意选项,不同模板接受的键集合不同;
  • template-schema:模板数据 JSON Schema 的 URL,默认值为{{.Template}}.schema.json,即自动在模板路径后追加.schema.json来发现 Schema;
  • require-template-schema-exists:默认true,若 Schema 下载失败则 mockery 报错;设为false则跳过 Schema 校验;
  • formatter:生成产物的格式化方式,取值gofmt/goimports/noop。

例如在配置文件中声明:

template: matryer

或在配置中同时使用template-data为模板提供附加信息。这些参数的默认值定义在 config/config.go 的DefaultConfig中(Template: addr("testify")、RequireTemplateSchemaExists: addr(true)、TemplateData: map[string]any{}),并通过ParseTemplates支持模板变量递归展开(如{{.InterfaceFile | base}}、{{.Template}}等,模板数据来自config.TemplateData结构体,参见 config/config.go)。

5. 如何复现与验证

该夹具的测试以*_test.go形式与生成产物同目录存放,可直接运行验证:

cd internal/fixtures/multi_template go test ./...

TestFoo会分别构造MockTestifyFoo与MockMatryerFoo并断言两者行为一致。若想亲身体验"同一接口、两套模板"的生成过程,可以在项目中用 mockery 对Foo分别指定template: testify与template: matryer生成到不同文件,或直接参考本夹具中被提交的两份生成产物作为对照基线——它们本身就是多模板渲染能力的可运行证据。

6. 小结

multi_template 夹具虽小,却浓缩了 mockery 最核心的设计理念:mockery 是模板渲染引擎,而非单一风格的 mock 工具。通过 internal/template_generator.go 中嵌入模板注册表与统一的Generate流水线,template配置参数可以自由切换内置的matryer、testify风格,甚至接入file://、https://自定义模板;同一接口因此可以产出函数注入式、期望匹配式乃至任意自定义形态的代码。无论是日常测试还是基于接口的代码生成框架二次开发,理解这套"模板即生成策略"的机制都是掌握 mockery 的关键一步。

  • 开发工具
  • 代码生成
  • 测试

【免费下载链接】mockery

A mock code autogenerator for Go

项目地址:https://gitcode.com/gh_mirrors/moc/mockery
点击查看免费下载
上一篇:gpui-kit Accordion 组件完全指南:折叠面板的构建、交互与源码剖析
下一篇:Wasp 应用自托管部署到 Caprover 完整指南:GitHub Actions + GHCR 全自动 CI/CD

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

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

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

立即咨询