- Add principle 27: 'Language-Agnostic Auto-Trigger' to engineering-principles - Remove hardcoded English-only keyword lists from all 26 skill descriptions - Replace with concept-based detection: triggers from code context, project files, and file types regardless of spoken language - Update detect-project.sh hook output with language-agnostic message
5.0 KiB
5.0 KiB
name, description
| name | description |
|---|---|
| testing | 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. Detects from code context and project files — not dependent on specific language keywords." |
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
- Don't write production code until you have a failing test.
- Don't write more of a test than is sufficient to fail.
- 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)
// 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
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:
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
// ❌ 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 — 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.gofiles. Table-driven tests for multiple cases.