xgo

xgo

革新 Go 测试的多功能工具集

xgo 为 Go 语言开发者提供了一套综合测试解决方案。作为 go 命令的预处理器,xgo 支持函数模拟、代码追踪和执行拦截,无需额外接口即可实现灵活测试。它还提供测试资源管理器和增量覆盖率分析等工具,简化单元测试、调试和性能分析流程。xgo 确保并发安全,适用于各种规模的 Go 项目测试需求。

xgoGo测试工具单元测试Mock功能代码覆盖率Github开源项目

xgo

Go 参考 Go 报告卡 Slack 小部件 Go 覆盖率 CI Awesome Go

English | 简体中文

xgo 为 golang 提供全in一的测试工具,包括:

对于猴子补丁部分,xgo 作为 go rungo buildgo test 的预处理器工作(参见我们的博客)。

更多详情请参阅快速开始文档

Xgo 测试浏览器

Xgo 测试浏览器

顺便说一句,我保证这是一个有趣的项目。

安装

go install github.com/xhd2015/xgo/cmd/xgo@latest

验证安装:

xgo version # 输出: # 1.0.x xgo help # 输出:帮助信息

如果找不到 xgo,你可能需要检查 $GOPATH/bin 是否已添加到你的 PATH 变量中。

对于 CI 作业(如 github 工作流),请参阅 doc/INSTALLATION.md

要求

xgo 需要至少 go1.17 才能编译。

对操作系统和架构没有特定限制。

所有操作系统和架构只要被 go 支持,就都被 xgo 支持。

x86x86_64 (amd64)arm64任何其他架构...
LinuxYYYY
WindowsYYYY
macOSYYYY
任何其他操作系统...YYYY

快速开始

让我们用 xgo 编写一个单元测试:

  1. 确保你已按照安装部分安装了 xgo,并通过以下命令验证安装:
xgo version # 输出 # 1.0.x
  1. 初始化一个 go 项目:
mkdir demo cd demo go mod init demo
  1. 添加 demo_test.go
package demo_test import ( "testing" "github.com/xhd2015/xgo/runtime/mock" ) func MyFunc() string { return "my func" } func TestFuncMock(t *testing.T) { mock.Patch(MyFunc, func() string { return "mock func" }) text := MyFunc() if text != "mock func" { t.Fatalf("expect MyFunc() to be 'mock func', actual: %s", text) } }
  1. 获取 github.com/xhd2015/xgo/runtime 依赖:
go get github.com/xhd2015/xgo/runtime
  1. 测试代码:
xgo test -v ./ # 注意:xgo 首次设置时会花一些时间

输出:

=== RUN TestFuncMock --- PASS: TestFuncMock (0.00s) PASS ok demo

注意:使用 xgo 而不是 go 来测试你的代码。

在底层,xgo 预处理你的代码以添加模拟钩子,然后调用 go 完成剩余工作。

上述代码可以在 doc/demo 中找到。

API

Patch

在当前 goroutine 中修补给定函数。

API:

  • Patch(fn,replacer) func()

速查表:

// 包级函数 mock.Patch(SomeFunc, mockFunc) // 每个实例的方法 // 只有绑定的实例 `v` 会被模拟 // `v` 可以是结构体或接口 mock.Patch(v.Method, mockMethod) // 每个 TParam 的泛型函数 // 只有指定的 `int` 版本会被模拟 mock.Patch(GenericFunc[int], mockFuncInt) // 每个 TParam 和实例的泛型方法 v := GenericStruct[int] mock.Mock(v.Method, mockMethod) // 闭包也可以被模拟 // 使用较少,但也支持 mock.Mock(closure, mockFunc)

参数:

  • 如果 fn 是一个简单函数(即包级函数、类型拥有的函数或闭包(是的,我们确实支持模拟闭包)),那么对该函数的所有调用都将被拦截,
  • replacer 另一个将替换 fn 的函数

范围:

  • 如果 Patchinit 中调用,那么所有 goroutine 都将被模拟。
  • 否则,如果 Patchinit 之后调用,那么模拟拦截器只对当前 goroutine 有效,其他 goroutine 不受影响。

注意:fnreplacer 应该有相同的签名。

返回:

  • 一个 func() 可以用来在当前 goroutine 退出之前提前移除替换器

Patch 在当前 goroutine 中用 replacer 替换给定的 fn。一旦当前 goroutine 退出,它将移除替换器。

示例:

package patch_test import ( "testing" "github.com/xhd2015/xgo/runtime/mock" ) func greet(s string) string { return "hello " + s } func TestPatchFunc(t *testing.T) { mock.Patch(greet, func(s string) string { return "mock " + s }) res := greet("world") if res != "mock world" { t.Fatalf("expect patched result to be %q, actual: %q", "mock world", res) } }

注意:PatchMock(下文)支持顶级变量和常量,参见 runtime/mock/MOCK_VAR_CONST.md

模拟标准库的注意事项:模拟标准库函数有不同的模式,参见 runtime/mock/stdlib.md

Mock

runtime/mock 还提供了另一个名为 Mock 的 API,类似于 Patch

它们之间的唯一区别在于第二个参数:Mock 接受一个拦截器。

Mock 可以在 Patch 无法使用的情况下使用,比如具有未导出类型的函数。

API 详情:runtime/mock/README.md

Mock API:

  • Mock(fn, interceptor)

参数:

  • fnPatch 部分描述的相同
  • 如果 fn 是一个方法(即 file.Read),那么只有对该实例的调用会被拦截,其他实例不受影响

拦截器签名:func(ctx context.Context, fn *core.FuncInfo, args core.Object, results core.Object) error

  • 如果拦截器返回 nil,则目标函数被模拟,
  • 如果拦截器返回 mock.ErrCallOld,则目标函数会被再次调用,
  • 否则,拦截器返回非 nil 错误,该错误将被设置为函数的返回错误。

还有其他 2 个 API 可用于基于名称设置模拟,详情请查看 runtime/mock/README.md

方法模拟示例:

type MyStruct struct { name string } func (c *MyStruct) Name() string { return c.name } func TestMethodMock(t *testing.T){ myStruct := &MyStruct{ name: "my struct", } otherStruct := &MyStruct{ name: "other struct", } mock.Mock(myStruct.Name, func(ctx context.Context, fn *core.FuncInfo, args core.Object, results core.Object) error { results.GetFieldIndex(0).Set("mock struct") return nil }) // myStruct 受影响 name := myStruct.Name() if name!="mock struct"{ t.Fatalf("expect myStruct.Name() to be 'mock struct', actual: %s", name) } // otherStruct 不受影响 otherName := otherStruct.Name() if otherName!="other struct"{ t.Fatalf("expect otherStruct.Name() to be 'other struct', actual: %s", otherName) } }

Trace

Trace 可能是 xgo 提供的最强大的工具,这个博客有更详细的示例:https://blog.xhd2015.xyz/posts/xgo-trace_a-powerful-visualization-tool-in-go

在调试深层调用栈时会很痛苦。

Trace 通过收集分层堆栈跟踪并将其存储到文件中供以后使用来解决这个问题。

不用说,有了 Trace,调试变得不那么常见:

package trace_test import ( "fmt" "testing" ) func TestTrace(t *testing.T) { A() B() C() } func A() { fmt.Printf("A\n") } func B() { fmt.Printf("B\n");C(); } func C() { fmt.Printf("C\n") }

xgo 运行:

# 运行测试 # 这将把跟踪写入 TestTrace.json # --strace 表示堆栈跟踪 xgo test --strace ./ # 查看跟踪 xgo tool trace TestTrace.json

输出: trace html

来自 runtime/test/stack_trace/update_test.go 的另一个更复杂的示例: trace html

真实世界的例子:

Trace 帮助你快速上手新项目。

默认情况下,Trace 会将跟踪写入当前工作目录下的临时目录。可以通过将 XGO_TRACE_OUTPUT 设置为不同的值来覆盖此行为:

  • XGO_TRACE_OUTPUT=stdout:跟踪将写入标准输出,用于调试目的,
  • XGO_TRACE_OUTPUT=<dir>:跟踪将写入 <dir>
  • XGO_TRACE_OUTPUT=off:关闭跟踪。

除了 --strace 标志外,xgo 还允许你定义应该收集哪个跨度,使用 trace.Begin()

import "github.com/xhd2015/xgo/runtime/trace" func TestTrace(t *testing.T) { A() finish := trace.Begin() defer finish() B() C() }

跟踪将只包括 B()C()

Trap

Xgo 在调用 go 之前预处理源代码和 IR(中间表示),为用户提供在调用任何函数时拦截的机会。

Trap 允许开发人员动态拦截函数执行。 以下示例通过添加Trap拦截器来记录函数执行跟踪:

(查看 test/testdata/trap/trap.go 获取更多详情。)

package main import ( "context" "fmt" "github.com/xhd2015/xgo/runtime/core" "github.com/xhd2015/xgo/runtime/trap" ) func init() { trap.AddInterceptor(&trap.Interceptor{ Pre: func(ctx context.Context, f *core.FuncInfo, args core.Object, results core.Object) (interface{}, error) { if f.Name == "A" { fmt.Printf("trap A\n") return nil, nil } if f.Name == "B" { fmt.Printf("abort B\n") return nil, trap.ErrAbort } return nil, nil }, }) } func main() { A() B() } func A() { fmt.Printf("A\n") } func B() { fmt.Printf("B\n") }

使用 go 运行:

go run ./ # 输出: # A # B

使用 xgo 运行:

xgo run ./ # 输出: # trap A # A # abort B

AddInterceptor() 将给定的拦截器添加到全局或局部,取决于它是在 init 中调用还是在 init 之后调用:

  • init 之前: 对所有goroutine全局生效,
  • init 之后: 仅对当前goroutine生效,并在当前goroutine退出后清除。

AddInterceptor()init 之后调用时,它将返回一个dispose函数,用于在当前goroutine退出之前提前清除拦截器。

示例:

func main(){ clear := trap.AddInterceptor(...) defer clear() ... }

Trap还有一个名为 Direct(fn) 的辅助函数,可用于绕过任何trap和模拟拦截器,直接调用原始函数。

工具

测试资源管理器

xgo e 子命令将在浏览器中打开一个测试资源管理器UI,提供一种简单的方法来测试和调试go代码。

使用测试资源管理器时,会使用 xgo test 而不是 go test,以启用模拟功能。

$ xgo e 服务器监听于 http://localhost:7070
<img width="1792" alt="test-explorer" src="https://github.com/xhd2015/xgo/assets/14964938/f8689a2d-b422-4101-acef-0aa41ad7e246">

注:xgo exgo tool test-explorer 的别名。

如需帮助,运行 xgo e help

有关配置详情,请参阅 doc/test-explorer/README.md

增量覆盖率

xgo tool coverage 子命令扩展了go内置的 go tool cover,以实现更好的可视化。

首先,运行 go testxgo test 获取覆盖率配置文件:

go test -cover -coverpkg ./... -coverprofile cover.out ./...

然后,使用 xgo 显示覆盖率:

xgo tool coverage serve cover.out

输出:

coverage

显示的覆盖率是覆盖率和git差异的组合。默认情况下,只显示修改的行:

  • 已覆盖的行显示为浅蓝色,
  • 未覆盖的行显示为浅黄色

这有助于快速定位未覆盖的更改,并逐步为它们添加测试。

并发安全

我知道你们使用其他猴子补丁库的人因这些框架隐含的不安全性而困扰。

但我向你保证,xgo中的模拟是内置的并发安全的。这意味着,你可以随心所欲地并发运行多个测试。

为什么?当你运行一个测试时,你设置了一些模拟,这些模拟只会影响运行测试的goroutine。当goroutine结束时,无论测试通过还是失败,这些模拟都会被清除。

想知道为什么吗?敬请期待,我们正在编写内部文档。

实现细节

正在进行中...

有关更多详情,请参阅 Issue#7

这篇博客有一个基本解释: https://blog.xhd2015.xyz/posts/xgo-monkey-patching-in-go-using-toolexec

为什么选择 xgo?

原因很简单:没有接口。

是的,没有接口,仅用于模拟。如果抽象接口的唯一原因是为了模拟,那只会让我感到无聊,而不是工作。

仅为模拟而提取接口对我来说从不是一个选项。对于问题的领域来说,它仅仅是一个变通方法。它强制代码以一种风格编写,这就是我们不喜欢它的原因。

猴子补丁简单地为问题做了正确的事。但现有的库在兼容性方面表现不佳。

所以我创建了 xgo,希望它最终能取代其他解决模拟问题的方案。

xgomonkey 进行比较

项目 bouk/monkey 最初由bouk创建,如他的博客 https://bou.ke/blog/monkey-patching-in-go 所述。

简而言之,它使用低级汇编黑客技术在运行时替换函数。随着它被越来越广泛地使用(尤其是在macOS上),这给用户带来了许多令人困惑的问题。

后来,作者本人将其存档并不再维护。然而,后来有两个项目接管了ASM的想法,并为较新的go版本和架构(如Apple M1)添加了支持。

尽管如此,这两个项目并没有解决ASM引入的底层兼容性问题,包括跨平台支持、需要写入执行代码的只读部分以及缺乏通用模拟。

因此,开发人员仍然时不时会遇到令人烦恼的故障。

Xgo通过避免对语言本身进行低级黑客攻击来解决这些问题。相反,它依赖于go编译器使用的IR表示。

它在编译器编译源代码时对IR(中间表示)进行所谓的"IR重写"。IR比机器代码更接近源代码。因此,它比monkey解决方案要稳定得多。

总之,xgo 和monkey的比较如下:

xgomonkey
技术IRASM
函数模拟
未导出函数模拟
每实例方法模拟
每Goroutine模拟
每泛型类型模拟
变量模拟
常量模拟
闭包模拟
堆栈跟踪
通用陷阱
兼容性无限制限于amd64和arm64
API简单复杂
集成工作量容易困难

贡献

想要帮助贡献 xgo 吗?太好了!查看 CONTRIBUTING 获取帮助。

xgo 的演变

xgo 是原始 go-mock 的继任者,后者通过在编译前重写go代码来工作。

go-mock 采用的策略运行良好,但由于源代码膨胀,导致大型项目的构建时间大大延长。

然而,go-mock 在发现Trap、Trace和Mock之外的功能方面非常出色,还具有额外的能力,如捕获变量和禁用map随机性。

它是 xgo 所站立的肩膀。

编辑推荐精选

蛙蛙写作

蛙蛙写作

AI小说写作助手,一站式润色、改写、扩写

蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。

AI辅助写作AI工具蛙蛙写作AI写作工具学术助手办公助手营销助手AI助手
Trae

Trae

字节跳动发布的AI编程神器IDE

Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

AI工具TraeAI IDE协作生产力转型热门
问小白

问小白

全能AI智能助手,随时解答生活与工作的多样问题

问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。

热门AI助手AI对话AI工具聊天机器人
Transly

Transly

实时语音翻译/同声传译工具

Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。

讯飞智文

讯飞智文

一键生成PPT和Word,让学习生活更轻松

讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。

AI办公办公工具AI工具讯飞智文AI在线生成PPTAI撰写助手多语种文档生成AI自动配图热门
讯飞星火

讯飞星火

深度推理能力全新升级,全面对标OpenAI o1

科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。

热门AI开发模型训练AI工具讯飞星火大模型智能问答内容创作多语种支持智慧生活
Spark-TTS

Spark-TTS

一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型

Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。

咔片PPT

咔片PPT

AI助力,做PPT更简单!

咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。

讯飞绘文

讯飞绘文

选题、配图、成文,一站式创作,让内容运营更高效

讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。

热门AI辅助写作AI工具讯飞绘文内容运营AI创作个性化文章多平台分发AI助手
材料星

材料星

专业的AI公文写作平台,公文写作神器

AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。

下拉加载更多