skip to content

In Go, what do you trade away when a test fake embeds the interface it is meant to satisfy?

level: middleimportance: nice to knowfreq 30%

answer

  1. the embedded field is nil
  2. every method is promoted for free
  3. a build failure becomes something later
  4. the compiler stops listing which fakes need work
  5. nil pointer dereference at the first unoverridden call

basics

~20 s

You trade a compile error for a runtime panic. The embedded interface field is nil but supplies every method, so the fake always compiles; calling a method you did not override dereferences that nil interface and panics instead of failing to build.

solid answer

~50 s

Writing `type fakeStore struct{ UserStore }` promotes every method of the embedded interface onto the fake, so it satisfies `UserStore` no matter how the interface changes, and you override only the one method your test cares about. The convenience is real for a wide interface you do not control. The cost is the signal you lose: when someone adds a method to the interface, an explicit fake stops compiling and points at every test that must be updated, while the embedding fake compiles happily and panics with a nil pointer dereference the first time that method is called — possibly in a rarely-run case, and with a stack trace that does not obviously say "this fake is incomplete". For a one- or two-method consumer-side interface I write the methods out; embedding is a concession to interfaces that are too wide.

code

go · 8 lines
go
type fakeStore struct {
	UserStore // nil: promotes every method, implements none
	user User
}

func (f fakeStore) UserByID(ctx context.Context, id int64) (User, error) {
	return f.user, nil
}

go deeper

for a junior

Know that embedding an interface in a struct gives that struct all the interface's methods, and that leaving the embedded field nil means calling one of them panics rather than returning anything.

for a middle

Explain the mechanism: promotion puts the methods in the struct's method set, dispatch goes through the nil field, and the compile-time completeness check on your fakes is what you gave up.

for a senior

Weigh it in review: when a wide external interface makes embedding worth it, when a failing base struct is the better middle path, and how you keep a change to an interface from silently skipping test updates.

for a principal

Decide the house rule and the reason behind it — widespread embedding in fakes usually signals interfaces that describe implementations, and narrowing them removes the motive rather than banning the technique.

## The trick ```go type fakeStore struct { UserStore // embedded interface, left nil user User } func (f fakeStore) UserByID(ctx context.Context, id int64) (User, error) { return f.user, nil } ``` Embedding an interface in a struct promotes that interface's methods onto the struct's method set. The struct therefore satisfies the interface immediately, with zero methods written. Any method you *do* declare on the struct shadows the promoted one, so you implement only what the test exercises. ## What is actually stored The embedded field is an ordinary field named after the interface type, holding an interface value. Left out of the struct literal, it holds nil. The promoted methods are dispatched through that field, so calling one is a method call on a nil interface value: the program panics at runtime with `invalid memory address or nil pointer dereference`. That is the whole tradeoff in one sentence: satisfaction is guaranteed at compile time and correctness is deferred to runtime. ## The diagnostic you give up With an explicit fake, adding a method to the interface produces a build failure at the point where the fake is passed: the type no longer implements the interface, and the compiler lists the missing method. That error is a to-do list — it enumerates exactly which test files need attention before the change can land. It is one of the more useful compile errors in Go, and it is the reason a hand-written fake is a maintenance asset rather than a chore. Embed the interface and that error disappears. The new method is promoted, every fake still builds, and the suite still passes — until some case reaches code calling the new method, which panics. If the new call sits behind a condition few tests hit, the failure may not appear until much later, and the panic's stack trace points into the runtime, not at the fake's declaration. ## When embedding is nevertheless right Three situations justify it. First, an interface you do not control that is genuinely wide — writing and maintaining twenty stub methods is worse than the risk. Second, a fake used by many test files where only a couple of methods matter and the rest would be pure noise. Third, deliberately *seeking* the panic: some teams embed precisely so that any unanticipated call blows up loudly rather than returning zero values silently, which is a defensible bargain when the alternative is a fake that succeeds at everything. A middle path keeps both properties: embed a small base struct rather than the interface, where the base implements every method by failing the test. ```go type failingStore struct{ t *testing.T } func (f failingStore) UserByID(ctx context.Context, id int64) (User, error) { f.t.Fatalf("unexpected call to UserByID") return User{}, nil } type fakeStore struct { failingStore user User } ``` Now an unanticipated call fails the test with a readable message instead of panicking, and adding a method to the interface still breaks the *base* struct at compile time, which is exactly the one place you want to be told. ## The signal to read If embedding an interface into fakes has become the house style, that is usually evidence that the interfaces are too wide — describing an implementation rather than a consumer's need. Narrowing them to the one or two methods each caller actually uses removes the motive for the trick, and restores the compile error as your change-impact report. ## Two details worth knowing The promoted methods keep the embedded field's dynamic behaviour: if you *do* assign a real implementation into the embedded field, the fake delegates to it for everything you have not overridden. That makes the pattern a serviceable decorator — wrap a real collaborator and override one method — which is a legitimate use quite apart from testing. And a struct embedding an interface is not the same as a struct embedding a struct: with an interface, satisfaction is guaranteed but the implementation may be absent; with a struct, the methods are real code that always runs.

  • What exactly happens at runtime when a promoted method on a nil embedded interface is called?
    The call is dispatched through the embedded field, which holds a nil interface value with no dynamic type or value, so the program panics with a nil pointer dereference. It is a normal panic, recoverable in principle, and it surfaces wherever the call happens rather than where the fake was declared.
  • Is there a use for embedding an interface outside of tests?
    Yes. Assign a real implementation into the embedded field and the type delegates everything you have not overridden, which is a concise decorator: wrap a collaborator, change one method, forward the rest. The nil-panic hazard only exists when the field is deliberately left empty.
  • Your team embeds interfaces in most fakes. What does that suggest about the interfaces?
    That they are too wide — describing what an implementation can do rather than what a caller needs. A one- or two-method consumer-side interface is cheap to implement explicitly, so the incentive to embed disappears once the interfaces are narrowed.

saying these in an interview costs you the question

  • Thinks embedding an interface provides default method implementations
  • Expects a compile error when a method is added to the interface
  • Says the promoted method returns the zero value when unset
  • Treats embedding as free rather than a deferred failure
  • Embeds a wide interface instead of narrowing it