Xgo
All-in-one go testing library
Install / Use
/learn @xhd2015/XgoREADME
xgo
English | 简体中文
xgo provides all-in-one test utilities for golang, including:
As for the monkey patching part, xgo works as a preprocessor for go run,go build, and go test(see our blog).
See Quick Start and Documentation for more details.
By the way, I promise you this is an interesting project.
Installation
go install github.com/xhd2015/xgo/cmd/xgo@latest
Verify the installation:
xgo version
# output:
# 1.0.x
xgo help
# output: help messages
If xgo is not found, you may need to check if $GOPATH/bin is added to your PATH variable.
For CI jobs like github workflow, see doc/INSTALLATION.md.
Requirement
xgo requires at least go1.17 to compile.
There is no specific limitation on OS and Architecture.
All OS and Architectures are supported by xgo as long as they are supported by go.
| | x86 | x86_64 (amd64) | arm64 | any other Arch... |
|:---------|:-----------:|:-----------:|:-----------:|:-----------:|
| Linux | Y | Y | Y | Y |
| Windows | Y | Y | Y | Y |
| macOS | Y | Y | Y | Y |
| any other OS... | Y | Y | Y | Y|
Quick Start
Let's write a unit test with xgo:
- Ensure you have installed
xgoby following the Installation section, and verify the installation with:
xgo version
# output
# 1.0.x
- Init a go project:
mkdir demo
cd demo
go mod init demo
- Add
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)
}
}
- Get the
github.com/xhd2015/xgo/runtimedependency:
go get github.com/xhd2015/xgo/runtime
- Test the code:
xgo test -v ./
# NOTE: xgo will take some time to setup for the first time
Output:
=== RUN TestFuncMock
--- PASS: TestFuncMock (0.00s)
PASS
ok demo
NOTE: xgo is used to test your code, not just go.
Under the hood, xgo preprocess your code to add mock hooks, and then calls go to do remaining jobs.
The above code can be found at doc/demo.
API
Patch
Patch given function within current goroutine.
The API:
Patch(fn,replacer) func()
Cheatsheet:
// package level func
mock.Patch(SomeFunc, mockFunc)
// per-instance method
// only the bound instance `v` will be mocked
// `v` can be either a struct or an interface
mock.Patch(v.Method, mockMethod)
// per-TParam generic function
// only the specified `int` version will be mocked
mock.Patch(GenericFunc[int], mockFuncInt)
// per TParam and instance generic method
v := GenericStruct[int]
mock.Mock(v.Method, mockMethod)
// closure can also be mocked
// less used, but also supported
mock.Mock(closure, mockFunc)
Parameters:
- If
fnis a simple function(i.e. a package level function, or a function owned by a type, or a closure(yes, we do support mocking closures)),then all call to that function will be intercepted, replaceranother function that will replacefn
Scope:
- If
Patchis called frominit, then all goroutines will be mocked. - Otherwise,
Patchis called afterinit, then the mock interceptor will only be effective for current goroutine, other goroutines are not affected.
NOTE: fn and replacer should have the same signature.
Return:
- a
func()can be used to remove the replacer earlier before current goroutine exits
Patch replaces the given fn with replacer in current goroutine. It will remove the replacer once current goroutine exits.
Example:
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)
}
}
Patch(and Mock below) also works for package-level variables:
package patch
import (
"testing"
"github.com/xhd2015/xgo/runtime/mock"
)
var a int = 123
func TestPatchVarTest(t *testing.T) {
mock.Patch(&a, func() int {
return 456
})
b := a
if b != 456 {
t.Fatalf("expect patched variable a to be %d, actual: %d", 456, b)
}
}
See runtime/mock/MOCK_VAR_CONST.md.
Mock
runtime/mock also provides another API called Mock, which is similar to Patch.
The the only difference between them lies in the the second parameter: Mock accepts an interceptor.
Mock can be used where Patch cannot be used, such as functions with unexported type.
API details: runtime/mock/README.md
The Mock API:
Mock(fn, interceptor)
Parameters:
fnsame as described in Patch section- If
fnis a method(i.e.file.Read),then only call to the instance will be intercepted, other instances will not be affected
Interceptor Signature: func(ctx context.Context, fn *core.FuncInfo, args core.Object, results core.Object) error
- If the interceptor returns
nil, then the target function is mocked, - If the interceptor returns
mock.ErrCallOld, then the target function is called again, - Otherwise, the interceptor returns a non-nil error, that will be set to the function's return error.
There are other 2 APIs can be used to setup mock based on name, check runtime/mock/README.md for more details.
Method mock example:
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 is affected
name := myStruct.Name()
if name!="mock struct"{
t.Fatalf("expect myStruct.Name() to be 'mock struct', actual: %s", name)
}
// otherStruct is not affected
otherName := otherStruct.Name()
if otherName!="other struct"{
t.Fatalf("expect otherStruct.Name() to be 'other struct', actual: %s", otherName)
}
}
Trace
Trace might be the most powerful tool provided by xgo, this blog has a more thorough example: https://blog.xhd2015.xyz/posts/xgo-trace_a-powerful-visualization-tool-in-go
It is painful when debugging with a deep call stack.
Trace addresses this issue by collecting the hierarchical stack trace and stores it into file for later use.
Needless to say, with Trace, debug becomes less usual:
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") }
Run with xgo:
# run the test
# this will write the trace into TestTrace.json
# --strace represents stack trace
xgo test --strace ./
# view the trace
xgo tool trace TestTrace.json
Output:

Another more complicated example from runtime/test/stack_trace/update_test.go:

Real world examples:
- https://github.com/Shibbaz/GOEventBus/pull/11
Trace helps you get started to a new project quickly.
By default, Trace will write traces to a temp directory under current working directory. This behavior can be overridden by setting --strace-dir=<DIR>.
Besides the --strace flag, xgo allows you to define which span should be collected, using trace.Trace():
import "github.com/xhd2015/xgo/runtime/trace"
func TestTrace(t *testing.T) {
A()
trace.Trace(trace.Config{OutputFile:"demo.json"},nil,func() (interface{},error){
B()
C()
return nil,nil
})
}
The trace will only include B() and C().
Recorder
The following example logs function execution trace by adding a Recorder:
package main
import (
"context"
"fmt"
"github.com/xhd2015/xgo/runtime/trace"
)
func main() {
trace.RecordCall(A, func(){
fmt.Printf("record A\n")
})
trace.RecordCall(B,func(){
fmt.Printf("record B\n")
})
A()
B()
}
func A() {
fmt.Pri
Related Skills
gh-issues
337.4kFetch GitHub issues, spawn sub-agents to implement fixes and open PRs, then monitor and address PR review comments. Usage: /gh-issues [owner/repo] [--label bug] [--limit 5] [--milestone v1.0] [--assignee @me] [--fork user/repo] [--watch] [--interval 5] [--reviews-only] [--cron] [--dry-run] [--model glm-5] [--notify-channel -1002381931352]
node-connect
337.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
xurl
337.4kA CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint.
frontend-design
83.2kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
