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

136 lines
5.4 KiB
Markdown

# 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// ❌ 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-specification**`expect(mock).toHaveBeenCalledTimes(1)` when "at least once" is fine
-**Conditional mocks**`mockReturnValueOnce` chains that break when order changes
-**Partial mocks** — mocking some methods but not others on the real object (spy is better)