Project Icon

xgo

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

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

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
test-explorer

注: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 所站立的肩膀。

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

稿定AI

稿定设计 是一个多功能的在线设计和创意平台,提供广泛的设计工具和资源,以满足不同用户的需求。从专业的图形设计师到普通用户,无论是进行图片处理、智能抠图、H5页面制作还是视频剪辑,稿定设计都能提供简单、高效的解决方案。该平台以其用户友好的界面和强大的功能集合,帮助用户轻松实现创意设计。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号