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.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: testing
|
||||
description: Best practices for software testing — TDD, test pyramid, F.I.R.S.T. principles, mocking strategies, and test organization. Use when writing tests, designing test strategy, refactoring under test, or whenever the user mentions "unit test," "integration test," "TDD," "test coverage," "mock," "stub," "e2e test," "Jest," "Vitest," "pytest," "cargo test," or "testing."
|
||||
---
|
||||
|
||||
# Testing Best Practices
|
||||
|
||||
## The Test Pyramid
|
||||
|
||||
```
|
||||
╱╲
|
||||
╱ E2E ╲ Few — critical user journeys
|
||||
╱────────╲
|
||||
╱ Integration ╲ Some — API, DB, external service boundaries
|
||||
╱────────────────╲
|
||||
╱ Unit Tests ╲ Many — domain logic, utilities, pure functions
|
||||
╱────────────────────╲
|
||||
```
|
||||
|
||||
- **Unit tests** — fast, isolated, test one behavior. 70%+ of tests.
|
||||
- **Integration tests** — test boundaries (DB queries, API contracts, file IO).
|
||||
- **E2E tests** — critical paths only. Slow and brittle — minimize.
|
||||
|
||||
## Three Laws of TDD
|
||||
|
||||
1. Don't write production code until you have a failing test.
|
||||
2. Don't write more of a test than is sufficient to fail.
|
||||
3. Don't write more production code than is sufficient to pass.
|
||||
|
||||
The cycle: Red (failing test) → Green (passing) → Refactor.
|
||||
|
||||
## F.I.R.S.T. Principles
|
||||
|
||||
- **Fast** — tests run quickly. If slow, they won't be run.
|
||||
- **Independent** — no test depends on another. Any order, any subset.
|
||||
- **Repeatable** — same result every time, in any environment.
|
||||
- **Self-validating** — pass/fail is binary. No manual inspection.
|
||||
- **Timely** — written *before* (or at the same time as) production code.
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Arrange-Act-Assert (AAA)
|
||||
```typescript
|
||||
// Arrange
|
||||
const user = new User('test@example.com');
|
||||
const service = new AuthService(mockRepo);
|
||||
|
||||
// Act
|
||||
const result = await service.login(user);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
```
|
||||
|
||||
### Naming
|
||||
```typescript
|
||||
describe('CreateOrderUseCase', () => {
|
||||
it('throws when inventory is insufficient', async () => { ... });
|
||||
it('creates order with correct total', async () => { ... });
|
||||
it('deducts inventory on successful order', async () => { ... });
|
||||
});
|
||||
```
|
||||
|
||||
### One assertion per test? No — one *concept* per test.
|
||||
Group related assertions for the same behavior:
|
||||
```typescript
|
||||
it('returns complete user profile', () => {
|
||||
const profile = service.getProfile(userId);
|
||||
expect(profile.name).toBe('Alice');
|
||||
expect(profile.email).toBe('alice@example.com');
|
||||
expect(profile.role).toBe('admin');
|
||||
});
|
||||
```
|
||||
|
||||
## What to Test
|
||||
|
||||
| Test | What | Example |
|
||||
|------|------|---------|
|
||||
| Domain logic | Business rules, calculations, validations | `PriceCalculator.calculateTotal()` |
|
||||
| Edge cases | Empty state, null, max values, error paths | `Order.create({ items: [] })` |
|
||||
| Public contracts | API endpoints, method signatures | `POST /orders returns 201` |
|
||||
| Error handling | Expected failures, retry, fallback | `Repository.save() when DB down` |
|
||||
|
||||
## What NOT to Test
|
||||
|
||||
- ❌ Framework internals (React, Express, Drizzle — they have their own tests)
|
||||
- ❌ Implementation details (private methods — test through public API)
|
||||
- ❌ Simple one-liners with no logic (`getters`, `toString`)
|
||||
- ❌ Configuration constants
|
||||
|
||||
## Mocking Strategies
|
||||
|
||||
- **Mock external boundaries only** — database, network, filesystem, clock.
|
||||
- **Don't mock domain objects** — use real entities/value objects.
|
||||
- **Mock roles, not objects** — mock the interface/port, not the concrete class.
|
||||
- **Prefer fakes over mocks** for test doubles that have real behavior (e.g., in-memory DB).
|
||||
- **Over-mocking is a smell** — tests that break on every refactor are testing implementation, not behavior.
|
||||
|
||||
### Mock Levels
|
||||
```typescript
|
||||
// ❌ Over-mocked: tests break on refactor, test internal wiring
|
||||
const mockRepo = { save: vi.fn() };
|
||||
const useCase = new CreateOrder(mockRepo);
|
||||
mockRepo.save.mockResolvedValueOnce({ id: '1' });
|
||||
|
||||
// ✅ Better: test behavior through real fakes
|
||||
class InMemoryOrderRepo implements OrderRepository {
|
||||
private orders = new Map<string, Order>();
|
||||
async save(o: Order) { this.orders.set(o.id, o); return o; }
|
||||
async findById(id: string) { return this.orders.get(id) ?? null; }
|
||||
}
|
||||
```
|
||||
|
||||
## Test Coverage Guidelines
|
||||
|
||||
- **80-90% line coverage** is healthy for production code
|
||||
- **100% is a red flag** — likely testing trivia and implementation details
|
||||
- **Focus coverage on domain logic** (business rules) over infrastructure wrappers
|
||||
- **Coverage is a lagging indicator** — good tests aren't about coverage, they're about confidence
|
||||
|
||||
## Deeper Reference
|
||||
|
||||
When the task calls for it, load:
|
||||
|
||||
- **[references/mocks.md](references/mocks.md)** — Complete test double taxonomy (Dummy, Fake, Stub, Mock, Spy). Code examples, when to use each, over-mocking traps, and anti-patterns.
|
||||
|
||||
## Language-Specific Directives
|
||||
|
||||
- **TypeScript:** Use Vitest over Jest (faster, ESM-native). Use `vi.fn()` sparingly.
|
||||
- **Python:** Use pytest (not unittest). Fixtures over setup/teardown.
|
||||
- **Rust:** Unit tests inline in module. Integration tests in `tests/` directory.
|
||||
- **Go:** Tests in `_test.go` files. Table-driven tests for multiple cases.
|
||||
@@ -0,0 +1,135 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user