Interface Mocks¶
Rewire mocks Go interfaces via rewire.NewMock[T]. The toolexec wrapper synthesizes a concrete backing struct at compile time, triggered purely by referencing the interface in a test file. No go:generate, no committed mock files, no separate CLI invocation.
Quick start¶
Just reference the interface in a test and the toolexec wrapper emits a backing struct at compile time.
package foo_test
import (
"testing"
"github.com/example/bar"
"github.com/GiGurra/rewire/pkg/rewire"
)
func TestService_GreetingFlow(t *testing.T) {
greeter := rewire.NewMock[bar.GreeterIface](t)
rewire.InstanceFunc(t, greeter, bar.GreeterIface.Greet, func(g bar.GreeterIface, name string) string {
return "mocked: " + name
})
svc := NewService(greeter)
got := svc.HelloFlow("Alice")
// ...
}
Stubs are per-instance, so two mocks of the same interface are independent:
g1 := rewire.NewMock[bar.GreeterIface](t)
g2 := rewire.NewMock[bar.GreeterIface](t)
rewire.InstanceFunc(t, g1, bar.GreeterIface.Greet, func(g bar.GreeterIface, name string) string { return "g1: " + name })
rewire.InstanceFunc(t, g2, bar.GreeterIface.Greet, func(g bar.GreeterIface, name string) string { return "g2: " + name })
g1.Greet("Alice") // "g1: Alice"
g2.Greet("Bob") // "g2: Bob"
Unstubbed methods return zero values:
greeter := rewire.NewMock[bar.GreeterIface](t)
greeter.Greet("Alice") // "" — no stub, returns the zero value
Clear every stub on a mock with rewire.RestoreInstance:
Individual stubs can be cleared with rewire.RestoreInstanceFunc(t, greeter, bar.GreeterIface.Greet).
How it works¶
When the toolexec wrapper compiles your test package, it scans _test.go files for rewire.NewMock[X] references. For each interface it finds, it locates the interface's source, parses the method set, and synthesizes a backing struct into the test package's compile args:
// Synthesized at compile time, never written to disk:
type _rewire_mock_bar_GreeterIface struct{ _ [1]byte }
var Mock__rewire_mock_bar_GreeterIface_Greet_ByInstance sync.Map
func (m *_rewire_mock_bar_GreeterIface) Greet(name string) (_r0 string) {
// per-instance dispatch — same mechanism that backs
// rewire.InstanceFunc for rewritten concrete methods.
...
}
func init() {
rewire.RegisterMockFactory("github.com/example/bar.GreeterIface", func() any {
return &_rewire_mock_bar_GreeterIface{}
})
rewire.RegisterByInstance(
"github.com/example/bar.GreeterIface.Greet",
&Mock__rewire_mock_bar_GreeterIface_Greet_ByInstance,
)
}
rewire.NewMock[bar.GreeterIface](t) looks up the factory by the interface's fully-qualified name and returns a fresh instance typed as bar.GreeterIface. The generated method's body consults the per-instance dispatch table — the exact same ByInstance mechanism that backs per-instance method mocks.
Current scope¶
Supported today:
- Non-generic interfaces — any number of methods, any signature
- Generic interfaces — single and multi-type-parameter, with arbitrary type arguments:
- Builtins (
int,string,bool, etc.) - Slices, maps, channels, function types
- Pointers (
*time.Time) - External package types (
context.Context,*http.Request) - Nested generic instantiations (
Container[Container[int]]) - Types from the test package itself (
Container[*User])
- Builtins (
- Methods using imported types —
context.Context,io.Reader, etc. - Methods referencing same-package types as bare identifiers — an interface in
bar/can return*Greeter(without qualifying it as*bar.Greeter), and the generator automatically qualifies it when synthesizing the backing struct into the test package. - Dot imports in the interface's declaring file —
import . "pkg"brings the dot-imported package's exported names into the file's top-level scope; the generator detects the dot import, lists the dot-imported package's exported types, and qualifies bare identifiers with the dot-imported alias (soReaderresolves toio.Reader, notdeclaringpkg.Reader). Bare-ident embeds pointing at dot-imported interfaces are handled the same way — they're treated as cross-package embeds. - Module-aware package resolution —
replacedirectives ingo.mod, workspace files (go.work), and vendor directories are all honored when locating an interface's source. Package lookup goes throughgo listso rewire's resolution is in lock-step with the surrounding Go build system. - Variadic parameters, multi-return, unnamed parameters
- Multiple mocks of the same interface — scoped independently via per-instance dispatch
- Multiple instantiations of the same generic interface —
Container[int]andContainer[string]produce distinct backing structs and don't collide - Embedded interfaces — same-file, same-package, and cross-package embeds all work. The full promoted method set is materialized on the mock, including methods from stdlib embeds like
io.Reader. Generic embeds where the outer interface's type parameter flows into the embed are supported (e.g.Outer[U]embeddingBase[U]instantiated asOuter[int]gives aBase[int]method set).
// All of these work:
g := rewire.NewMock[bar.GreeterIface](t) // non-generic
ci := rewire.NewMock[bar.Container[int]](t) // generic, single type arg
cs := rewire.NewMock[bar.Container[string]](t) // distinct instantiation
c := rewire.NewMock[bar.Cache[string, int]](t) // multi type args
n := rewire.NewMock[bar.Container[bar.Container[int]]](t) // nested generic
e := rewire.NewMock[bar.Container[time.Duration]](t) // external package type arg
rc := rewire.NewMock[bar.ReadCloser](t) // embeds io.Reader + same-pkg Named
lr := rewire.NewMock[bar.ListRepo[int]](t) // generic embed: ListRepo[U] embeds Base[U]
gf := rewire.NewMock[bar.GreeterFactory](t) // bare same-pkg *Greeter auto-qualified
Stubbing a promoted method uses the OUTER interface as the receiver in the method expression — that's what Go's runtime reports for method expressions on types with embeds:
rc := rewire.NewMock[bar.ReadCloser](t)
// Read is promoted from io.Reader but stubbed via bar.ReadCloser.Read:
rewire.InstanceFunc(t, rc, bar.ReadCloser.Read, func(r bar.ReadCloser, p []byte) (int, error) {
return copy(p, "hi"), nil
})
Trade-offs¶
IDE visibility. The synthesized backing struct only exists during compilation. Gopls and other tooling can't see it. We deliberately designed the API so users never need to name the struct — you pass rewire.NewMock[bar.GreeterIface] for creation and bar.GreeterIface.Greet for stubbing, both of which the IDE understands. In practice the generated type is invisible and the cost disappears.
Build speed. At compile time, rewire reads the interface's source and synthesizes a backing-struct file per instantiation. This adds a small per-test-package overhead proportional to the number of mocked interfaces. Negligible in practice, but not free.
Reviewability. There's no committed mock_*.go file to eyeball, by design. If you ever need to see what the toolexec emitted, the temporary directory passed to the compiler is logged on errors and the synthesized file lives there until the compile finishes.