xgo:一个基于 IR 重写的 golang mock 库

2024 年 3 月 28 日
 xhd2015

最近基于工作需要,在使用了 gomonkey 遇到很多问题之后,并且也担心基于 Assembly 的方式对未来单测维护的不可持续性,开发了 xgo 。

xgo 基于 IR 中间码重写,更加贴近源代码而不是机器码。所以兼容性比 Assembly 要好很多,功能也更强。

下面是一个简单的示例:

package demo

import (
	"context"
	"testing"

	"github.com/xhd2015/xgo/runtime/core"
	"github.com/xhd2015/xgo/runtime/mock"
)

func MyFunc() string {
	return "my func"
}
func TestFuncMock(t *testing.T) {
	mock.Mock(MyFunc, func(ctx context.Context, fn *core.FuncInfo, args core.Object, results core.Object) error {
		results.GetFieldIndex(0).Set("mock func")
		return nil
	})
	text := MyFunc()
	if text != "mock func" {
		t.Fatalf("expect MyFunc() to be 'mock func', actual: %s", text)
	}
}

欢迎大家使用和提意见

项目地址 https://github.com/xhd2015/xgo

2295 次点击
所在节点    Go 编程语言
12 条回复
mightybruce
2024 年 3 月 28 日
行,关注了。
xhd2015
2024 年 3 月 28 日
说明一下:该 mock 不需要基于接口进行 monkey ,开箱可用😃
oceana
2024 年 3 月 28 日
点个赞
xhd2015
2024 年 3 月 28 日
@oceana 感谢,可以一起交流😌
sophos
2024 年 3 月 28 日
有点意思,和我这个项目的功能有点类似,不过实现思路不太一样 :-)

https://github.com/go-kod/kod
xhd2015
2024 年 3 月 28 日
@sophos 目标不一样,这个是单元测试场景,主打无代码侵入,无代码生成
sophos
2024 年 3 月 28 日
@xhd2015 嗯,我这个是为了优化代码模块设计,同时解决单元测试标准化的问题

不过个人感觉单元测试的最佳实践还是 mock interface ,patch 不够直观
xhd2015
2024 年 3 月 28 日
@sophos 哈哈,我持相反的看法
502Chef
2025 年 6 月 11 日
看文档是支持泛型函数指定类型 mock 的,但是自己尝试下来确不支持,求解答,谢谢

报错:

panic: func not instrumented by xgo, see https://github.com/xhd2015/xgo/tree/master/doc/ERR_NOT_INSTRUMENTED.md: demo_test.MyFuncT[int]



```go
package demo_test

import (
"testing"

"github.com/xhd2015/xgo/runtime/mock"
)

func MyFunc() string {
return "my func"
}

func MyFuncT[T any](t T) T {
return t
}

var MyFuncTInt = MyFuncT[int]

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)
}
}

func TestFuncMockIntA(t *testing.T) {
mock.Patch(MyFuncTInt, func(t int) int {
return 2
})
text := MyFuncTInt(1)
if text != 2 {
t.Fatalf("expect intA() to be '2', actual: %d", text)
}
}

func TestFuncMockT(t *testing.T) {
println("111111")
mock.Patch(MyFuncTInt, func(t int) int {
println("333333")
return 2
})
println("222222")
text := MyFuncTInt(1)
if text != 2 {
t.Fatalf("expect MyFuncT() to be '2', actual: %d", text)
}
}
```

```console
xgo test -v ./
=== RUN TestFuncMock
--- PASS: TestFuncMock (0.00s)
=== RUN TestFuncMockIntA
--- FAIL: TestFuncMockIntA (0.00s)
panic: func not instrumented by xgo, see https://github.com/xhd2015/xgo/tree/master/doc/ERR_NOT_INSTRUMENTED.md: demo_test.MyFuncT[int] [recovered]
panic: func not instrumented by xgo, see https://github.com/xhd2015/xgo/tree/master/doc/ERR_NOT_INSTRUMENTED.md: demo_test.MyFuncT[int]

goroutine 19 [running]:
testing.tRunner.func1.2({0x100c85be0, 0x140000a0ac0})
/Users/lula/.xgo/go-instrument/go1.24.2_op_ho_Ce_go_1._li_9f53f045/go1.24.2/src/testing/testing.go:1734 +0x1ac
testing.tRunner.func1()
/Users/lula/.xgo/go-instrument/go1.24.2_op_ho_Ce_go_1._li_9f53f045/go1.24.2/src/testing/testing.go:1737 +0x334
panic({0x100c85be0?, 0x140000a0ac0?})
/Users/lula/.xgo/go-instrument/go1.24.2_op_ho_Ce_go_1._li_9f53f045/go1.24.2/src/runtime/panic.go:792 +0x124
github.com/xhd2015/xgo/runtime/internal/trap.Inspect({0x100c75480, 0x100cb6540})
/Users/lula/Library/Go/pkg/mod/github.com/xhd2015/xgo/runtime@v1.1.7/internal/trap/inspect.go:68 +0x530
github.com/xhd2015/xgo/runtime/internal/trap.pushMockReplacer({0x100c75480, 0x100cb6540}, {0x100c75480, 0x100cb6550})
/Users/lula/Library/Go/pkg/mod/github.com/xhd2015/xgo/runtime@v1.1.7/internal/trap/mock.go:160 +0x3d0
github.com/xhd2015/xgo/runtime/internal/trap.PushMockReplacer(...)
/Users/lula/Library/Go/pkg/mod/github.com/xhd2015/xgo/runtime@v1.1.7/internal/trap/mock.go:47
github.com/xhd2015/xgo/runtime/mock.Patch(...)
/Users/lula/Library/Go/pkg/mod/github.com/xhd2015/xgo/runtime@v1.1.7/mock/patch.go:15
demo_test.TestFuncMockIntA(0x14000146380)
/Users/lula/Code/Go/go-learn/demo/demo_test.go:30 +0x14c
testing.tRunner(0x14000146380, 0x100cb5c50)
/Users/lula/.xgo/go-instrument/go1.24.2_op_ho_Ce_go_1._li_9f53f045/go1.24.2/src/testing/testing.go:1792 +0xe4
created by testing.(*T).Run in goroutine 1
/Users/lula/.xgo/go-instrument/go1.24.2_op_ho_Ce_go_1._li_9f53f045/go1.24.2/src/testing/testing.go:1851 +0x374
FAIL demo 0.259s
FAIL
```
502Chef
2025 年 6 月 11 日
第三个 Test 函数代码贴错了,纠正一下,下面的日志没有错

➜ demo xgo version
1.1.7
➜ demo xgo exec go version
go version go1.24.2 darwin/arm64
➜ demo go env
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOVERSION='go1.24.2'

```go
package demo_test

import (
"testing"

"github.com/xhd2015/xgo/runtime/mock"
)

func MyFunc() string {
return "my func"
}

func MyFuncT[T any](t T) T {
return t
}

var MyFuncTInt = MyFuncT[int]

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)
}
}

func TestFuncMockIntA(t *testing.T) {
mock.Patch(MyFuncTInt, func(t int) int {
return 2
})
text := MyFuncTInt(1)
if text != 2 {
t.Fatalf("expect intA() to be '2', actual: %d", text)
}
}

func TestFuncMockT(t *testing.T) {
println("111111")
mock.Patch(MyFuncT[int], func(t int) int {
println("333333")
return 2
})
println("222222")
text := MyFuncTInt(1)
if text != 2 {
t.Fatalf("expect MyFuncT() to be '2', actual: %d", text)
}
}
```
xhd2015
2025 年 6 月 13 日
@huyujievip 感谢反馈,我今天看下这个问题。根据这个测试用例,范型是支持的 https://github.com/xhd2015/xgo/blob/master/runtime/test/mock/mock_generic/mock_generic_go1.20_above_should_success_test.go
xhd2015
2025 年 6 月 19 日
@huyujievip 第二个函数失败是因为 MyFuncTInt 是一个变量,而不是函数

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1027638

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX