en un clic
testing
// Testing patterns for behavior-driven tests. Use when writing tests, creating test factories, structuring test files, or deciding what to test. Do NOT use for UI-specific testing (see front-end-testing or react-testing skills).
// Testing patterns for behavior-driven tests. Use when writing tests, creating test factories, structuring test files, or deciding what to test. Do NOT use for UI-specific testing (see front-end-testing or react-testing skills).
Turn broad requirements, large stories, epics, features, initiatives, or backlog items into small end-to-end child stories without turning them into technical component tasks. Use when refining a backlog, decomposing epics, planning an MVP or walking skeleton, looking for vertical slices, reducing story size, applying SPIDR/Hamburger/capability slicing, avoiding scatter-gather/component stories, or deciding the first valuable story before implementation planning.
Adversarially review written stories, plans, acceptance criteria, specs, and design mocks to surface missing states, unhandled edge cases, unstated assumptions, unverifiable criteria, and slices that are still too broad or horizontal — then work interactively with the user, one question at a time, to turn each gap into a new acceptance criterion, plan update, mock-state spec, or recommendation to use story-splitting. Use when an existing artifact needs tightening before planning or coding.
Set up and run mutation testing with Stryker, including full-project and diff-against-main runs, then use surviving mutants to strengthen weak or missing tests.
Planning work as vertical slices in small, known-good increments. Use when starting significant work, turning already-split stories into PR-sized implementation plans, planning PRs, or sequencing complex tasks. If the input is a broad story, epic, feature idea, or backlog item that still needs product slicing, use story-splitting first.
Test-Driven Development workflow. Use for ALL code changes - features, bug fixes, refactoring. TDD is non-negotiable.
Produce a mock-audit storyboard — a single HTML page embedding every UX surface in a scope of work side-by-side, with per-mock audit checklists, a flow diagram, and gap cards for missing mocks. Use before any feature touching multiple UX surfaces begins implementation. Also use when the user says "make it easy for me to see all these mocks in one place", "audit the mocks", or "I want to review the whole flow".
| name | testing |
| description | Testing patterns for behavior-driven tests. Use when writing tests, creating test factories, structuring test files, or deciding what to test. Do NOT use for UI-specific testing (see front-end-testing or react-testing skills). |
For verifying test effectiveness through mutation analysis, load the mutation-testing skill. Use its mutator rules while planning and writing tests, not only after tests exist. For evaluating test quality against Dave Farley's properties, load the test-design-reviewer skill.
Test behavior, not implementation. 100% coverage through business behavior, not implementation details.
Example: Validation code in payment-validator.ts gets 100% coverage by testing processPayment() behavior, NOT by directly testing validator functions.
When planning or writing tests, automatically scan the intended behavior and changed production code against the mutator rules from the mutation-testing skill's resources/mutator-rules.md resource. A good test should fail if a realistic mutant changes the behavior.
Load that resource when the code under test includes conditionals, arithmetic, equality, boolean logic, array/string operations, optional chaining, or meaningful side effects. Use it to identify likely surviving mutants before the Stryker run.
When the scan finds an obvious gap, add or strengthen a behavior test immediately. When the gap depends on product or domain judgment, use the harness's ask-question facility before choosing a test. Ask one concise question with concrete choices, explain the potential mutant, and state the tradeoff.
Example ask-question prompt:
The discount rule uses `subtotal >= 100`, but current tests only cover `150`.
Should the exact `100` boundary receive the discount?
- Yes: add a boundary test for `100`
- No: change/confirm the rule as `subtotal > 100`
- Unspecified: document the behavior as intentionally not guaranteed
Do not ask when the gap is plainly a missing assertion, missing boundary, missing branch, or missing side-effect check. Fix those directly.
Never test implementation details. Test behavior through public APIs.
Why this matters:
❌ WRONG - Testing implementation:
// ❌ Testing HOW (implementation detail)
it('should call validateAmount', () => {
const spy = jest.spyOn(validator, 'validateAmount');
processPayment(payment);
expect(spy).toHaveBeenCalled(); // Tests HOW, not WHAT
});
// ❌ Testing private methods
it('should validate CVV format', () => {
const result = validator._validateCVV('123'); // Private method!
expect(result).toBe(true);
});
// ❌ Testing internal state
it('should set isValidated flag', () => {
processPayment(payment);
expect(processor.isValidated).toBe(true); // Internal state
});
✅ CORRECT - Testing behavior through public API:
it('should reject negative amounts', () => {
const payment = getMockPayment({ amount: -100 });
const result = processPayment(payment);
expect(result.success).toBe(false);
expect(result.error).toContain('Amount must be positive');
});
it('should reject invalid CVV', () => {
const payment = getMockPayment({ cvv: '12' }); // Only 2 digits
const result = processPayment(payment);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid CVV');
});
it('should process valid payments', () => {
const payment = getMockPayment({ amount: 100, cvv: '123' });
const result = processPayment(payment);
expect(result.success).toBe(true);
expect(result.data.transactionId).toBeDefined();
});
Validation code gets 100% coverage by testing the behavior it protects:
// Tests covering validation WITHOUT testing validator directly
describe('processPayment', () => {
it('should reject negative amounts', () => {
const payment = getMockPayment({ amount: -100 });
const result = processPayment(payment);
expect(result.success).toBe(false);
});
it('should reject amounts over 10000', () => {
const payment = getMockPayment({ amount: 15000 });
const result = processPayment(payment);
expect(result.success).toBe(false);
});
it('should reject invalid CVV', () => {
const payment = getMockPayment({ cvv: '12' });
const result = processPayment(payment);
expect(result.success).toBe(false);
});
it('should process valid payments', () => {
const payment = getMockPayment({ amount: 100, cvv: '123' });
const result = processPayment(payment);
expect(result.success).toBe(true);
});
});
// ✅ Result: payment-validator.ts has 100% coverage through behavior
Key insight: When coverage drops, ask "What business behavior am I not testing?" not "What line am I missing?"
Never extract a function into its own file purely to give it its own unit test. Extract for readability (a descriptive name clarifies intent), for DRY (same knowledge used in multiple places — see the refactoring skill's "DRY = Knowledge, Not Code" rule), or for separation of concerns. Not for testability.
If code is inline in a function, it gets coverage through that function's behavioral tests. Every layer has behavioral tests — domain functions have vitest unit tests, components have browser tests, pages have integration tests. There is no gap.
The anti-pattern is creating a 1:1 mapping between extracted helpers and test files (see "No 1:1 Mapping" below). The extracted helper is an implementation detail of its consumer. Test the consumer's behavior.
❌ WRONG — Extracted single-use helper with its own test file:
// prepare-participant-data.ts (new file, one caller)
export const prepareParticipantData = (items: Item[]) => ({
yourClaims: items.filter(i => i.isClaimed && i.isClaimedByCurrentUser),
available: items.filter(i => !i.isClaimedByCurrentUser),
});
// prepare-participant-data.test.ts (tests the helper directly)
it('filters claims', () => { ... });
✅ CORRECT — Inline in the consuming function, tested through its behavior:
// load-participant-view.ts
export const loadParticipantView = async (db, eventId, userId) => {
const items = await getItems(db, eventId);
const yourClaims = items.filter(i => i.isClaimed && i.isClaimedByCurrentUser);
const available = items.filter(i => !i.isClaimedByCurrentUser);
return { yourClaims, available };
};
// The behavioral test for loadParticipantView covers the filtering:
it('returns claimed gifts in yourClaims and unclaimed in available', () => {
const result = await loadParticipantView(db, eventId, userId);
expect(result.yourClaims).toHaveLength(1);
expect(result.available).toHaveLength(2);
});
When extraction IS justified (DRY): If the same filtering logic is used by multiple consumers with the same business meaning, extract it. But test it through each consumer's behavior, not as an isolated unit.
For test data, use factory functions with optional overrides.
Partial<T> overrides for customizationlet/beforeEach - use factories for fresh stateconst getMockUser = (overrides?: Partial<User>): User => {
return UserSchema.parse({
id: 'user-123',
name: 'Test User',
email: 'test@example.com',
role: 'user',
...overrides,
});
};
// Usage
it('creates user with custom email', () => {
const user = getMockUser({ email: 'custom@example.com' });
const result = createUser(user);
expect(result.success).toBe(true);
});
import { UserSchema } from '@/schemas'; // Import real schema
const getMockUser = (overrides?: Partial<User>): User => {
return UserSchema.parse({
id: 'user-123',
name: 'Test User',
email: 'test@example.com',
role: 'user',
isActive: true,
createdAt: new Date('2024-01-01'),
...overrides,
});
};
Why validate with schema?
Tip: For factories where only a subset of fields are relevant, use Pick<T, 'field1' | 'field2'> for the overrides parameter to constrain what callers can customize.
For nested objects, compose factories:
const getMockItem = (overrides?: Partial<Item>): Item => {
return ItemSchema.parse({
id: 'item-1',
name: 'Test Item',
price: 100,
...overrides,
});
};
const getMockOrder = (overrides?: Partial<Order>): Order => {
return OrderSchema.parse({
id: 'order-1',
items: [getMockItem()], // ✅ Compose factories
customer: getMockCustomer(), // ✅ Compose factories
payment: getMockPayment(), // ✅ Compose factories
...overrides,
});
};
// Usage - override nested objects
it('calculates total with multiple items', () => {
const order = getMockOrder({
items: [
getMockItem({ price: 100 }),
getMockItem({ price: 200 }),
],
});
expect(calculateTotal(order)).toBe(300);
});
❌ WRONG: Using let and beforeEach
let user: User;
beforeEach(() => {
user = { id: 'user-123', name: 'Test User', ... }; // Shared mutable state!
});
it('test 1', () => {
user.name = 'Modified User'; // Mutates shared state
});
it('test 2', () => {
expect(user.name).toBe('Test User'); // Fails! Modified by test 1
});
✅ CORRECT: Factory per test
it('test 1', () => {
const user = getMockUser({ name: 'Modified User' }); // Fresh state
// ...
});
it('test 2', () => {
const user = getMockUser(); // Fresh state, not affected by test 1
expect(user.name).toBe('Test User'); // ✅ Passes
});
❌ WRONG: Incomplete objects
const getMockUser = () => ({
id: 'user-123', // Missing name, email, role!
});
✅ CORRECT: Complete objects
const getMockUser = (overrides?: Partial<User>): User => {
return UserSchema.parse({
id: 'user-123',
name: 'Test User',
email: 'test@example.com',
role: 'user',
...overrides, // All required fields present
});
};
❌ WRONG: Redefining schemas in tests
// ❌ Schema already defined in src/schemas/user.ts!
const UserSchema = z.object({ ... });
const getMockUser = () => UserSchema.parse({ ... });
✅ CORRECT: Import real schema
import { UserSchema } from '@/schemas/user';
const getMockUser = (overrides?: Partial<User>): User => {
return UserSchema.parse({
id: 'user-123',
name: 'Test User',
email: 'test@example.com',
...overrides,
});
};
Watch for these patterns that give fake 100% coverage:
❌ WRONG - Gives 100% coverage but tests nothing:
it('calls validator', () => {
const spy = jest.spyOn(validator, 'validate');
validate(payment);
expect(spy).toHaveBeenCalled(); // Meaningless assertion
});
✅ CORRECT - Test actual behavior:
it('should reject invalid payment', () => {
const payment = getMockPayment({ amount: -100 });
const result = validate(payment);
expect(result.success).toBe(false);
expect(result.error).toContain('Amount must be positive');
});
❌ WRONG - No behavior validation:
it('processes payment', () => {
const spy = jest.spyOn(processor, 'process');
handlePayment(payment);
expect(spy).toHaveBeenCalledWith(payment); // So what?
});
✅ CORRECT - Verify the outcome:
it('should process payment and return transaction ID', () => {
const payment = getMockPayment();
const result = handlePayment(payment);
expect(result.success).toBe(true);
expect(result.transactionId).toBeDefined();
});
❌ WRONG - Testing implementation, not behavior:
it('sets amount', () => {
payment.setAmount(100);
expect(payment.getAmount()).toBe(100); // Trivial
});
✅ CORRECT - Test meaningful behavior:
it('should calculate total with tax', () => {
const order = createOrder({ items: [item1, item2] });
const total = order.calculateTotal();
expect(total).toBe(230); // 200 + 15% tax
});
❌ WRONG - Missing edge cases:
it('validates payment', () => {
const result = validate(getMockPayment());
expect(result.success).toBe(true); // Only happy path!
});
// Missing: negative amounts, invalid CVV, missing fields, etc.
✅ CORRECT - Test all branches:
describe('validate payment', () => {
it('should reject negative amounts', () => {
const payment = getMockPayment({ amount: -100 });
expect(validate(payment).success).toBe(false);
});
it('should reject amounts over limit', () => {
const payment = getMockPayment({ amount: 15000 });
expect(validate(payment).success).toBe(false);
});
it('should reject invalid CVV', () => {
const payment = getMockPayment({ cvv: '12' });
expect(validate(payment).success).toBe(false);
});
it('should accept valid payments', () => {
const payment = getMockPayment();
expect(validate(payment).success).toBe(true);
});
});
Don't create test files that mirror implementation files.
❌ WRONG:
src/
payment-validator.ts
payment-processor.ts
payment-formatter.ts
tests/
payment-validator.test.ts ← 1:1 mapping
payment-processor.test.ts ← 1:1 mapping
payment-formatter.test.ts ← 1:1 mapping
✅ CORRECT:
src/
payment-validator.ts
payment-processor.ts
payment-formatter.ts
tests/
process-payment.test.ts ← Tests behavior, not implementation files
Why: Implementation details can be refactored without changing tests. Tests verify behavior remains correct regardless of how code is organized internally.
When writing tests, verify:
let/beforeEach - use factories for fresh state