Method Mocking¶
Replace struct methods at test time using Go's method expression syntax. Like function mocking, no interfaces or code changes needed.
Pointer receiver methods¶
func TestGreetWith_MockedMethod(t *testing.T) {
rewire.Func(t, (*bar.Greeter).Greet, func(g *bar.Greeter, name string) string {
return "Mocked, " + name
})
g := &bar.Greeter{Prefix: "Hi"}
got := GreetWith(g, "Alice")
// got == "Mocked, Alice"
}
The syntax (*bar.Greeter).Greet is a Go method expression. It has type func(*bar.Greeter, string) string — the receiver becomes the first parameter. Your replacement function must match this signature.
Value receiver methods¶
func TestPoint_String(t *testing.T) {
rewire.Func(t, bar.Point.String, func(p bar.Point) string {
return "mocked point"
})
}
For value receivers, use Type.Method (no parentheses or star).
Global by default¶
Method mocks set via rewire.Func apply to all instances of the type, not a specific object. When you mock (*Greeter).Greet, every *Greeter in the test uses the replacement:
rewire.Func(t, (*bar.Greeter).Greet, func(g *bar.Greeter, name string) string {
return "mocked"
})
g1 := &bar.Greeter{Prefix: "Hi"}
g2 := &bar.Greeter{Prefix: "Hey"}
g1.Greet("Alice") // "mocked"
g2.Greet("Bob") // also "mocked"
This is consistent with how function mocking works — the mock variable is package-level. If you want per-instance scoping without changing your production code to take an interface, see Per-instance method mocks below.
Per-instance method mocks¶
rewire.InstanceFunc scopes a method replacement to one specific receiver. Other instances of the same type keep running the real implementation (or the global mock, if one is set).
func TestInstanceFunc_ScopedToOneInstance(t *testing.T) {
g1 := &bar.Greeter{Prefix: "Hi"}
g2 := &bar.Greeter{Prefix: "Hello"}
rewire.InstanceFunc(t, g1, (*bar.Greeter).Greet, func(g *bar.Greeter, name string) string {
return "g1-mock: " + name
})
g1.Greet("Alice") // "g1-mock: Alice" — per-instance mock
g2.Greet("Bob") // "Hello, Bob!" — real body
}
The replacement's signature is the same as a rewire.Func method replacement: the receiver is the first parameter, followed by the method's own parameters.
Dispatch order¶
Inside a method wrapper, rewire checks three places in order:
- Per-instance mock (set via
rewire.InstanceFunc) — matched by the receiver pointer. - Global mock (set via
rewire.Func) — applies to any instance with no per-instance mock. - Real implementation — unchanged production code.
A per-instance mock always overrides a global mock for the same target:
func TestInstanceFunc_OverridesGlobal(t *testing.T) {
g1 := &bar.Greeter{Prefix: "Hi"}
g2 := &bar.Greeter{Prefix: "Hello"}
rewire.Func(t, (*bar.Greeter).Greet, func(g *bar.Greeter, name string) string {
return "global: " + name
})
rewire.InstanceFunc(t, g1, (*bar.Greeter).Greet, func(g *bar.Greeter, name string) string {
return "g1-mock: " + name
})
g1.Greet("Alice") // "g1-mock: Alice" — per-instance wins
g2.Greet("Bob") // "global: Bob" — global fallback
}
Multiple methods on one instance¶
You can set per-instance mocks for several methods on the same receiver. They're independent entries:
rewire.InstanceFunc(t, g, (*bar.Greeter).Greet, mockGreet)
rewire.InstanceFunc(t, g, (*bar.Greeter).Farewell, mockFarewell)
Mid-test restore¶
Each InstanceFunc call registers its own t.Cleanup, so per-instance mocks are automatically restored at test end. Two helpers let you clear mocks earlier:
// Clear every per-instance mock bound to this instance, for all methods.
rewire.RestoreInstance(t, g)
// Clear one specific per-instance entry, leaving other methods untouched.
rewire.RestoreInstanceFunc(t, g, (*bar.Greeter).Greet)
For global method mocks installed via rewire.Func, use rewire.RestoreFunc(t, (*bar.Greeter).Greet) — different verb, clearer intent.
Generic methods¶
Per-instance mocking works for methods on generic types too. Scope applies per instantiation and per receiver:
func TestInstanceFunc_GenericMethod(t *testing.T) {
c1 := &bar.Container[int]{}
c2 := &bar.Container[int]{}
rewire.InstanceFunc(t, c1, (*bar.Container[int]).Add, func(c *bar.Container[int], v int) {
// swallow — c1 never actually appends
})
c1.Add(1) // routed to per-instance mock
c2.Add(2) // real body — c2.Len() == 1
}
Interface equality on the per-instance key compares both dynamic type and pointer value, so *Container[int] and *Container[string] entries never collide even at the same address.
Restrictions¶
- Pointer receivers only. Value-receiver methods are copied on every call and have no stable identity to key on. The test fails immediately with a clear error.
- Free functions are not supported. There's nothing to scope to — use
rewire.Funcinstead. - Requires
-toolexec=rewire. The compiler wrapper has to be emitted with per-instance support, which happens automatically whenever any test in the module references the target viaInstanceFuncorRestoreInstanceFunc.
Method expressions vs. method values¶
Rewire requires you to pass a Go method expression ((*Type).Method or Type.Method), not a method value (instance.Method bound to a specific receiver):
g := &bar.Greeter{Prefix: "Hi"}
// ❌ Method value — bound to g. Rejected with a diagnostic.
rewire.Func(t, g.Greet, replacement)
// ✅ Method expression — unbound, receiver is first parameter.
rewire.Func(t, (*bar.Greeter).Greet, replacement)
Two reasons for the restriction:
- Semantic clarity. Method mocks are global — they apply to every instance of the type. Passing
g.Greetsuggests "mock only this instance", which isn't what actually happens. The method-expression form makes the global scope visible in the code. - Runtime identity.
runtime.FuncForPCreports method values with a-fmsuffix (e.g.,bar.(*Greeter).Greet-fm) because Go wraps them in a thunk that binds the receiver. That wrapper is not the function rewire rewrote, so the mock variable lookup would fail anyway.
If you accidentally pass a method value, rewire detects it and fails the test with a targeted error pointing to the method-expression form you should use instead.
Methods on generic types¶
Methods on generic types (func (c *Container[T]) Add(v T)) are supported, and each type-argument combination is mocked independently — same API as non-generic methods:
// Production: example/bar/bar.go
type Container[T any] struct {
items []T
}
func (c *Container[T]) Add(v T) {
c.items = append(c.items, v)
}
// Test
func TestContainer_MockInt(t *testing.T) {
rewire.Func(t, (*bar.Container[int]).Add, func(c *bar.Container[int], v int) {
// mock body — doesn't delegate, so items stays empty
})
ci := &bar.Container[int]{}
ci.Add(1)
ci.Add(2)
if ci.Len() != 0 {
t.Errorf("int mock should have swallowed adds, got Len=%d", ci.Len())
}
// A different instantiation is untouched
cs := &bar.Container[string]{}
cs.Add("hello")
if cs.Len() != 1 {
t.Errorf("string instantiation should run real, got Len=%d", cs.Len())
}
}
rewire.Real and rewire.RestoreFunc also work for generic methods. rewire.Real(t, (*bar.Container[int]).Add) returns a func(*bar.Container[int], int) that invokes the real method — pass the receiver as the first argument to call it.
The only thing rewire can't rewrite is a method that declares its own type parameters (func (c *C) Method[X any]()) — Go 1.18+ forbids that anyway, so it's not a practical gap.
Closure capture¶
Just like function mocks, method replacements are closures:
func TestDB_QueryTracking(t *testing.T) {
var queries []string
rewire.Func(t, (*bar.DB).Query, func(db *bar.DB, sql string, args ...any) ([]string, error) {
queries = append(queries, sql)
return nil, nil
})
// ... code that calls db.Query ...
if len(queries) != 2 {
t.Errorf("expected 2 queries, got %d", len(queries))
}
}
Requirements¶
Method mocking requires the toolexec wrapper, same as function mocking:
See Setup for IDE and terminal configuration.