Mocking can unlock fast, focused tests — or it can create brittle, misleading ones. The difference is what you mock and why.
Fake: a lightweight in-memory substitute that behaves like the real thing at the API level.
Pros: expressive, fewer expectations, refactor-safe, documents the contract.
// Interface boundary
type UserStore interface {
Save(ctx context.Context, u User) error
Find(ctx context.Context, email string) (User, error)
}
// Fake implementation for tests
type fakeUserStore struct {
saved []User
byEmail map[string]User
err error
}
func (f *fakeUserStore) Save(ctx context.Context, u User) error {
if f.err != nil { return f.err }
if f.byEmail == nil { f.byEmail = map[string]User{} }
f.byEmail[u.Email] = u
f.saved = append(f.saved, u)
return nil
}
func (f *fakeUserStore) Find(ctx context.Context, email string) (User, error) {
u, ok := f.byEmail[email]
if !ok { return User{}, sql.ErrNoRows }
return u, nil
}
Use this fake to assert behavioral outcomes (what was saved / retrieved), not the internal SQL the repo used.
Stubbing low-level Do(req)
calls with brittle expectations on URLs/headers/body formatting.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/pay" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
io.WriteString(w, `{"status":"ok","id":"123"}`)
}))
defer srv.Close()
cli := NewPaymentsClient(srv.URL)
res, err := cli.Pay(ctx, 499, "usd")
if err != nil { t.Fatal(err) }
if res.Status != "ok" { t.Fatalf("status: %s", res.Status) }
import { test } from 'node:test';
import assert from 'node:assert';
import { createServer } from 'http';
test('charges successfully', async () => {
const srv = createServer((req,res) => {
res.end(JSON.stringify({status:'ok', id:'123'}));
});
await new Promise(r => srv.listen(0, r));
const url = `http://127.0.0.1:${srv.address().port}`;
const client = new PaymentsClient(url);
const res = await client.pay(499, 'usd');
assert.equal(res.status, 'ok');
srv.close();
});
You're exercising real HTTP semantics without the network. No brittle spy-on-internals.
time.Sleep(2 * time.Second)
if got := svc.TokenExpired(u); !got {
t.Fatal("expected expired")
}
// Inject clock
type Clock interface{ Now() time.Time }
type fixedClock struct{ t time.Time }
func (f fixedClock) Now() time.Time { return f.t }
svc := Service{Clock: fixedClock{t: time.Date(2025,8,1,0,0,0,0,time.UTC)}}
// advance by setting a new fixed time
svc.Clock = fixedClock{t: time.Date(2025,8,2,0,0,0,0,time.UTC)}
if !svc.TokenExpired(u) { t.Fatal("expected expired") }
Deterministic, instant, no flakes.
type IDGen interface{ New() string }
type seqGen struct{ n int }
func (g *seqGen) New() string {
g.n++
return fmt.Sprintf("id-%d", g.n)
}
svc := Service{IDs: &seqGen{}}
order := svc.CreateOrder(...)
if !strings.HasPrefix(order.ID, "id-") {
t.Fatal("bad id")
}
Ask these before mocking:
👉 Mock boundaries; fake collaborators; assert behavior.
👉 If a harmless refactor breaks your test, you mocked the wrong thing.