A Go handler's tests all pass with a hand-written fake, yet production fails on lookup errors. Why?
answer
- green tests, one path exercised
- what does the stand-in ever return?
- zero value plus nil error is an answer
- let the case decide the outcome
- an unconfigured call should fail loudly
basics
~20 sThe fake's method returns the zero value and a nil error for every call, so the code's error branch never executes in any test. Give the fake per-case return values, add cases where it fails, and make an unconfigured call fail the test loudly.
solid answer
~40 sA stand-in written as `func (fakeStore) UserByID(...) (User, error) { return User{}, nil }` answers every call successfully, so no test ever reaches the `if err != nil` branch — the error handling is unexercised code that first runs in production. Two fixes, and I want both. First, make the fake configurable: give it fields, or function fields, so each case declares what the collaborator does, and write table cases that return a real error, including the not-found sentinel the real store returns. Second, make silence loud: hold a `*testing.T` in the fake and call `t.Fatalf` when a method is called that the case did not configure, so an unexpected call is a failure rather than a zero value. Then confirm with coverage that the error branch actually runs.
code
go · 5 linestype fakeStore struct{}
func (fakeStore) UserByID(ctx context.Context, id int64) (User, error) {
return User{}, nil // every id succeeds; the error branch never runs
}go deeper
Know that a stand-in returning zero values and a nil error makes every test take the success path. Give it fields so a test can make it fail, and write at least one failing case.
Explain the mechanics of a configurable double: fields or function fields per case, an unconfigured call failing the test, and returning the same sentinel error the real implementation returns so error mapping is exercised.
Demonstrate the diagnosis: read the fakes first, ask which can fail, confirm with a coverage profile that the branch runs, and assert on the behaviour the failure produces rather than on the error being non-nil.
Set the expectation that a test double which cannot fail is treated as a review defect, and decide how much fidelity to the real dependency the team's fakes owe before an integration test is the honest answer instead.
## The failure Every test is green. Then a lookup fails in production and the endpoint returns a 500 with a useless body, or worse, a 200 with an empty user. The tests could not have caught it, because the stand-in they used never failed. ```go type fakeStore struct{} func (fakeStore) UserByID(ctx context.Context, id int64) (User, error) { return User{}, nil // every id "succeeds" } ``` This is the most common defect in hand-written test doubles, and it is a *silent* one: nothing is wrong with the code, the interface or the wiring. The double simply encodes an assumption — "the collaborator works" — and every test inherits it. ## Why zero values are so dangerous here Go's zero values make a method trivial to stub, which is exactly the trap. `User{}` is a perfectly usable value and `nil` is a perfectly usable error, so the code under test proceeds happily. There is no exception to notice, no null reference to blow up. If the service is supposed to distinguish "no such user" from "the database is down", neither path is ever taken, and the mapping from a store error to a status code — usually the whole point of the handler — is untested. The second-order effect is worse: a zero-value fake also lies about *arguments*. If the code passes the wrong id, or forgets to pass the request's context, the fake answers just the same. ## Fix 1 — the case decides the behaviour Give the fake fields, or function fields when a case needs to vary by argument or count calls: ```go type fakeStore struct { t *testing.T userByID func(ctx context.Context, id int64) (User, error) } func (f fakeStore) UserByID(ctx context.Context, id int64) (User, error) { f.t.Helper() if f.userByID == nil { f.t.Fatalf("unexpected call to UserByID(%d)", id) } return f.userByID(ctx, id) } ``` Two things happen here. The behaviour moves into the test case, where a reader can see it beside the assertion. And an unconfigured call becomes a failure instead of a zero value — the fake now says "you called something this case did not expect" rather than quietly succeeding. (If the collaborator may be called from another goroutine, use `t.Errorf` and return an error instead: `t.Fatalf` must be called from the goroutine running the test.) ## Fix 2 — write the failing cases A table with a `storeErr` column, one row per outcome, is the usual shape: a success, the not-found sentinel the real store returns, and a generic failure. Assert on what the code *does* with each — the status code, the wrapped error, the log — not merely that an error came back. Use `errors.Is` so that wrapping does not break the assertion, and check that the handler does not leak the raw store error text to the client. ## Fix 3 — verify the branch actually ran Coverage is the cheap confirmation: `go test -cover`, or `-coverprofile` plus `go tool cover -html` to look at the handler and see whether the error branch is coloured as executed. This is one of the few places where a coverage number is genuinely diagnostic rather than decorative, because the question is binary — did any test enter this branch at all? ## Fix 4 — make the fake resemble the real thing where it matters The real store returns `sql.ErrNoRows` for a missing row; if your code special-cases it, at least one fake must return that exact sentinel, or the special case is never exercised. Similarly, if the real implementation honours context cancellation, one case should return `context.Canceled` and assert the code does not treat it as a client error. The point is not to reimplement the database in the fake; it is to reproduce the *distinctions the code under test makes*. ## A review heuristic When reviewing a package's tests, read the fakes first and ask which of them can fail. If none can, the suite is testing one path in a system whose difficulty lives in the others. A fake that cannot fail is not a test double, it is an assumption with a method set. ## Related trap The same fake that always succeeds also hides a *dropped* error: if the code under test ignores the returned error entirely, no failing case exists to reveal it. That is why the failing cases matter more than the passing one — the happy path is usually the one that would have been caught anyway, by the first person to run the service.
- How would you prove the error branch is now exercised?Run go test -coverprofile and open it with go tool cover -html, then look at the branch itself rather than the percentage. It is a binary question: did any case enter it? A quicker sanity check is to break the branch deliberately and confirm a test turns red.
- Your fake may be called from a goroutine the code under test starts. What changes?t.Fatalf must be called from the goroutine running the test, so from another goroutine use t.Errorf and return an error instead of stopping. Any counters the fake keeps also need a mutex, since the race detector will otherwise flag concurrent access from the test goroutine's assertions.
- Should the fake return the same sentinel error the real store returns?If the code under test distinguishes it, yes. A handler that maps sql.ErrNoRows to a 404 is only tested when some case returns exactly that value, checked with errors.Is. For paths where the code treats all failures alike, any error will do.
- How do you assert the code passed the right arguments without over-specifying?Have the function field check what matters and record nothing else: verify the id, and that the context is the one the caller was given rather than a fresh Background. Recording every call and asserting an exact sequence pins the implementation and makes harmless refactors fail.
saying these in an interview costs you the question
- Says a passing suite proves the error handling works
- Writes a stand-in that returns zero values for every call
- Only tests the happy path because the fake cannot fail
- Treats an unexpected call to the fake as harmless
- Asserts only that an error is non-nil, never which error
- Calls t.Fatalf from a goroutine other than the test's