Files
asepharyana-hub-guide/skills/testing/references/mocks.md
T
asepharyana e513cddd68 feat(hub-guide): expand plugin with 26 best-practice skills, hooks, and references
Transform hub-guide from a single-skill Hub monorepo guide into a
comprehensive programming best-practice plugin covering all situations.

Skills (26):
- Core: engineering-principles, clean-code, clean-architecture,
  design-patterns, testing, error-handling, security, api-design,
  git-workflow, documentation, logging-observability, performance
- Languages: typescript, python, rust, go
- Frameworks: react-frontend, elysiajs, hono-backend, drizzle-database, nextjs
- Infrastructure: docker, ci-cd, monitoring
- Monorepo: monorepo, hub-guide (existing)

Hooks:
- SessionStart: auto-detect project type and activate relevant skills
- PreToolUse (Write|Edit): inject language-specific rules per file type

Reference files for deep dives:
- clean-architecture/references/solid.md (SOLID + component principles)
- design-patterns/references/catalog.md (full GoF catalog with examples)
- testing/references/mocks.md (test double taxonomy)

Restructure plugin to modern skills/ directory format.
2026-07-25 11:35:16 +07:00

5.4 KiB

Test Doubles — Mock, Stub, Fake, Spy, Dummy

Understanding the differences prevents tests that are brittle, misleading, or hard to maintain.

The Taxonomy (Meszaros, xUnit Test Patterns)

Term What It Is When to Use
Dummy Passed but never used. Fills parameter lists. Satisfy constructor/parameter requirements that aren't exercised by this test.
Fake Working (but simplified) implementation. Uses real logic, just lighter. In-memory DB, fake HTTP client, fake file system. Preferred over mocks whenever possible.
Stub Returns canned answers to calls made during the test. When you need a consistent response (user exists, payment succeeded).
Mock Pre-programmed with expectations about what calls will be made. Verifies interactions. When you need to verify that something was called correctly (e.g., notification was sent).
Spy Records calls for later verification. Wraps a real object. When you want the real behavior but also need to verify calls.

The Continuum of Fidelity

Minimal ──────────────────────────────────────────────→ Max fidelity
Dummy → Stub → Spy → Mock → Fake (in-memory) → Real (integration)

Rule of thumb: Use the highest fidelity that's still fast and deterministic. Prefer Fakes → Stubs → Mocks → Dummies. Default to fakes.

Code Examples

Dummy

// Used only to satisfy type signature — never read in this test
it('creates order with items', async () => {
  const dummyNotifier = { send: vi.fn() }; // never called in this path
  const order = new CreateOrderUseCase(new InMemoryOrderRepo(), dummyNotifier);
  // ... test only cares about order creation, not notification
});

Fake

// Has real behavior, just in-memory. No DB, no network.
class FakeUserRepository implements UserRepository {
  private users = new Map<string, User>();

  async findById(id: string) { return this.users.get(id) ?? null; }
  async save(user: User) { this.users.set(user.id, user); return user; }
  async exists(email: string) { return [...this.users.values()].some(u => u.email === email); }
}

it('creates user', async () => {
  const repo = new FakeUserRepository();
  const svc = new UserService(repo);
  const user = await svc.create('a@b.com');
  expect(user.email).toBe('a@b.com');
  expect(await repo.exists('a@b.com')).toBe(true);
});

Stub

// Returns hardcoded answers — no real behavior, no verification
const stubbedRepo = {
  findById: vi.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
  save: vi.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};

it('returns user when found', async () => {
  const svc = new UserService(stubbedRepo);
  const user = await svc.findById('1');
  expect(user?.name).toBe('Alice');
});

Mock

// Sets expectations about interactions. Use sparingly.
it('sends notification on order', async () => {
  const notifyMock = vi.fn();
  const svc = new OrderService(new FakeOrderRepo(), notifyMock);
  await svc.create({ userId: '1', items: [...] });
  expect(notifyMock).toHaveBeenCalledWith('1', expect.stringContaining('order'));
});

Spy

// Wraps real behavior, records calls
const repo = new FakeUserRepository();
const spy = vi.spyOn(repo, 'save');
const svc = new UserService(repo);
await svc.create('a@b.com');
expect(spy).toHaveBeenCalledOnce();

When to Mock vs Use Fakes

Scenario Use
The collaborator is deterministic (math, calculation) Fake or real
The collaborator touches external systems (DB, network, disk) Fake (in-memory) or mock
You need to verify something was called Mock or spy
You need a consistent response Stub
The collaborator doesn't matter for this test Dummy or ignore

Mocking Best Practices

  1. Mock roles, not objects — mock the interface/port, not the concrete class.
  2. Don't mock domain objects — use real entities/value objects. They have no IO, so there's no reason to mock them.
  3. Over-mocking is a smell — if tests break on every refactor, you're testing implementation, not behavior.
  4. One mock per test, ideally — many mocks means many expectations, means fragile tests.
  5. Prefer mockResolvedValue (once) over mockResolvedValue (always) — be explicit about test context.

The Over-Mocking Trap

// ❌ Over-mocked — tests break when internals change
it('creates order', async () => {
  const repo = { save: vi.fn() };
  const calc = { calculate: vi.fn().mockReturnValue(100) };
  const notify = { send: vi.fn() };
  // ... mocks everywhere, tests know the implementation

// ✅ Better — fakes for real behavior, mock only for verification
it('creates order', async () => {
  const repo = new FakeOrderRepo();
  const calc = new PriceCalculator(); // real
  const notify = vi.fn(); // mock only what you need to verify
  // ...
});

Testing Anti-Patterns

  • Mocking everything — tests that don't test the real behavior
  • Mocking the SUT — mocking the class you're testing
  • Over-specificationexpect(mock).toHaveBeenCalledTimes(1) when "at least once" is fine
  • Conditional mocksmockReturnValueOnce chains that break when order changes
  • Partial mocks — mocking some methods but not others on the real object (spy is better)