enufstub
Inline interface stubs for Go tests — no code generation required.
The name comes from "enough" + "stub": this stub is enough.
Why?
Tools like gomock require go generate to produce mock files from interface definitions. In large codebases this adds up: every interface change triggers regeneration, CI pipelines run generators before tests, and reviewing diffs full of generated code is tedious. enufstub skips all of that — define stubs inline, right next to the test that uses them.
Installation
go get github.com/mickamy/enufstub
Requires Go 1.24+ and amd64 or arm64 architecture.
Quick Start
type UserRepository interface {
GetUser(id int) (User, error)
CreateUser(u User) error
DeleteUser(id int) error
}
func TestSomething(t *testing.T) {
stub := enufstub.Of[UserRepository]().
With("GetUser", func(id int) (User, error) {
return User{ID: id, Name: "Alice"}, nil
}).
With("CreateUser", func(u User) error {
return nil
}).
DefaultZero().
Build()
repo := stub.Impl()
user, _ := repo.GetUser(1) // returns User{ID: 1, Name: "Alice"}, nil
_ = repo.DeleteUser(1) // returns zero value (nil) via DefaultZero
}
API
Creating a Stub
enufstub.Of[T]() *Builder[T]
Creates a new stub builder for interface T. Panics if T is not an interface type.
Registering Methods
(*Builder[T]).With(method string, fn any) *Builder[T]
Registers an implementation for a method. fn must match the method's exact signature. Panics if the method doesn't exist or the signature doesn't match.
stub := enufstub.Of[Greeter]().
With("Greet", func(name string) string {
return "Hello, " + name
}).
Build()
Default Behavior for Unstubbed Methods
(*Builder[T]).DefaultPanic() *Builder[T] // panics (default)
(*Builder[T]).DefaultZero() *Builder[T] // returns zero values
(*Builder[T]).Default(fn) *Builder[T] // custom handler
DefaultPanic is the default — calling an unstubbed method panics with a descriptive message.
DefaultZero makes unstubbed methods return zero values for all return types.
Default accepts a custom handler:
stub := enufstub.Of[UserRepository]().
Default(func(method string, args []any) []any {
return nil // return zero values
}).
Build()
Building
(*Builder[T]).Build() *Stub[T]
Constructs the stub. Panics if proxy creation fails.
Using the Stub
(*Stub[T]).Impl() T
Returns the interface implementation to pass into your code under test.
Call Verification
Calls
(*Stub[T]).Calls(method string) []Call
Returns the call history for a method. Each Call has Args []any and Returns []any. Returns nil if the method was never called.
calls := stub.Calls("GetUser")
fmt.Println(calls[0].Args[0]) // first call's first argument
fmt.Println(calls[0].Returns[0]) // first call's first return value
Times
(*Stub[T]).Times(method string) int
Returns how many times a method was called.
if stub.Times("GetUser") != 2 {
t.Fatal("expected GetUser to be called twice")
}
CalledWith
(*Stub[T]).CalledWith(method string, args ...any) bool
Reports whether the method was called with matching arguments. Supports exact values (compared with reflect.DeepEqual) and matchers.
stub.CalledWith("GetUser", 42) // exact match
stub.CalledWith("GetUser", enufstub.Any()) // matches any value
stub.CalledWith("Do", 1, enufstub.Any()) // mix concrete and matcher
InOrder
enufstub.InOrder(groups ...[]Call) bool
Verifies that call groups occurred in sequential order. Each group's last call must precede the next group's first call.
_ = repo.CreateUser(User{Name: "Alice"})
_, _ = repo.GetUser(1)
_ = repo.DeleteUser(1)
enufstub.InOrder(
stub.Calls("CreateUser"),
stub.Calls("GetUser"),
stub.Calls("DeleteUser"),
) // true
Concurrency Safety
All call recording and verification methods are safe for concurrent use. You can call the stub from multiple goroutines without data races.
Constraints
String() method: Interfaces with a String() string method cannot be stubbed due to a limitation in the underlying proxy library.
- Method limit: Up to 250 methods per interface.
- Architecture: amd64 and arm64 only.
License
MIT