원클릭으로
test-driven-development
Use when implementing any feature or bugfix, before writing implementation code
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when implementing any feature or bugfix, before writing implementation code
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
MUST USE for ANY git operations. Enforces GPG-signed commits (-S flag always required; stop and ask user to unlock GPG agent on failure) and 50/72 commit message rule. Triggers: 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that'.
You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.
Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Use when executing implementation plans with independent tasks in the current session
SOC 직업 분류 기준
| name | test-driven-development |
| description | Use when implementing any feature or bugfix, before writing implementation code |
Write the test first. Watch it fail. Write minimal code to pass.
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
Violating the letter of the rules is violating the spirit of the rules.
Always:
Exceptions (ask your human partner):
Thinking "skip TDD just this once"? Stop. That's rationalization.
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
Write code before the test? Delete it. Start over.
No exceptions:
Implement fresh from tests. Period.
BEFORE writing your first test, understand what you're building:
Why this matters: Writing tests without context = testing the wrong thing or duplicating existing code.
Example:
// ❌ BAD: Jump straight to test without checking codebase
test('validates email format', () => {
expect(isValidEmail('test@example.com')).toBe(true);
});
// Codebase already has EmailValidator class you didn't find!
// ✅ GOOD: After 2-min search, found existing validator
test('validates email using EmailValidator', () => {
const validator = new EmailValidator();
expect(validator.isValid('test@example.com')).toBe(true);
});
// Consistent with codebase patterns
When to skip: Greenfield projects with no existing code. Otherwise, always understand context first.
After writing tests and minimal code, maintain quality through these practices:
| Principle | Guideline | Example |
|---|---|---|
| Follow established patterns | Match existing code style, structure, organization | ✅ Use same error handling pattern as rest of codebase ❌ Introduce new error pattern |
| Maintain consistency | Same naming conventions, file structure, module organization | ✅ getUserById() matches getPostById() ❌ fetchUser() when others use get*() |
| Keep functions focused | Single responsibility, one clear purpose | ✅ validateEmail() ❌ validateAndSaveEmail() |
| Prefer autonomous methods | Methods that don't rely on external state | ✅ Pure functions when possible ❌ Hidden dependencies |
After implementing each feature:
Avoid redundant work by understanding upfront:
| Type | Avoid | Instead |
|---|---|---|
| Comments | Explain WHAT code does (readable from code) | Explain WHY (business logic, tradeoffs, gotchas) |
| Tests | Multiple tests for same behavior | Consolidate similar tests, use parameterized tests |
| Code | Duplicate implementations | Understand constraints first, write once correctly |
| Rewrites | Multiple attempts due to misunderstanding | Read existing code, clarify requirements BEFORE coding |
Example:
// ❌ BAD: Comment explains WHAT (redundant with code)
// Check if user is authenticated
if (user.token && user.token.expiresAt > Date.now()) {
// ...
}
// ✅ GOOD: Comment explains WHY (adds context)
// Must check token expiry here, not in middleware, because
// admin routes bypass middleware but still need validation
if (user.token && user.token.expiresAt > Date.now()) {
// ...
}
Test consolidation:
// ❌ BAD: Redundant tests
test('validates email with @', () => { /*...*/ });
test('validates email with domain', () => { /*...*/ });
test('validates email with TLD', () => { /*...*/ });
// ✅ GOOD: Parameterized test
test.each([
['valid@example.com', true],
['missing-at.com', false],
['no-domain@', false],
['no-tld@example', false],
])('validates email format: %s → %s', (email, expected) => {
expect(isValidEmail(email)).toBe(expected);
});
flowchart LR
A[UNDERSTAND<br/>Context check]
B[RED<br/>Write failing test]
C{Verify fails<br/>correctly}
D[GREEN<br/>Minimal code]
E{Verify passes<br/>All green}
F[REFACTOR<br/>Clean up]
G((Next))
A --> B
B --> C
C -->|yes| D
C -->|wrong failure| B
D --> E
E -->|yes| F
E -->|no| D
F -->|stay green| E
E -->|next| G
G --> B
style A fill:#ffffcc
style B fill:#ffcccc
style D fill:#ccffcc
style F fill:#ccccff
Write one minimal test showing what should happen.
```typescript test('retries failed operations 3 times', async () => { let attempts = 0; const operation = () => { attempts++; if (attempts < 3) throw new Error('fail'); return 'success'; };const result = await retryOperation(operation);
expect(result).toBe('success'); expect(attempts).toBe(3); });
Clear name, tests real behavior, one thing
</Good>
<Bad>
```typescript
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
Vague name, tests mock not code
Requirements:
MANDATORY. Never skip.
npm test path/to/test.test.ts
Confirm:
Test passes? You're testing existing behavior. Fix test.
Test errors? Fix error, re-run until it fails correctly.
Write simplest code to pass the test.
```typescript async function retryOperation(fn: () => Promise): Promise { for (let i = 0; i < 3; i++) { try { return await fn(); } catch (e) { if (i === 2) throw e; } } throw new Error('unreachable'); } ``` Just enough to pass ```typescript async function retryOperation( fn: () => Promise, options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; onRetry?: (attempt: number) => void; } ): Promise { // YAGNI } ``` Over-engineeredDon't add features, refactor other code, or "improve" beyond the test.
MANDATORY.
npm test path/to/test.test.ts
Confirm:
Test fails? Fix code, not test.
Other tests fail? Fix now.
After green only:
Keep tests green. Don't add behavior.
Next failing test for next feature.
| Quality | Good | Bad |
|---|---|---|
| Minimal | One thing. "and" in name? Split it. | test('validates email and domain and whitespace') |
| Clear | Name describes behavior | test('test1') |
| Shows intent | Demonstrates desired API | Obscures what code should do |
Test data should mirror production reality:
| Principle | Guideline | Example |
|---|---|---|
| Mirrors production | Use realistic data structures, values, formats | ✅ {email: "user@example.com", age: 28} ❌ {email: "x", age: 1} |
| Full transformation chain | Test data through entire processing pipeline | ✅ Input → validate → transform → store → retrieve ❌ Test only transform step |
| Satisfies all constraints | Data passes ALL validation rules | ✅ Check required fields, formats, ranges ❌ Only test happy path |
| Includes edge cases | Empty strings, nulls, boundary values, special chars | ✅ ["", null, 0, -1, "🔥"] ❌ Only "test" |
Why this matters: Tests with toy data (x, y, foo, bar) pass but real data fails. Production-quality test data catches real bugs.
Example:
// ❌ BAD: Toy data that doesn't reflect reality
test('processes user', () => {
const user = { name: 'x', email: 'y' };
expect(process(user)).toBeTruthy();
});
// Missing: age validation, email format, required fields
// ✅ GOOD: Production-realistic data
test('processes valid user through full pipeline', () => {
const user = {
name: 'Alice Smith',
email: 'alice.smith@example.com',
age: 28,
preferences: { newsletter: true }
};
const result = process(user);
expect(result.id).toMatch(/^usr_[a-z0-9]+$/);
expect(result.name).toBe('Alice Smith');
expect(result.email).toBe('alice.smith@example.com');
});
test('rejects user with invalid email format', () => {
const user = {
name: 'Bob Jones',
email: 'not-an-email', // Edge case: malformed
age: 35
};
expect(() => process(user)).toThrow('Invalid email format');
});
test('handles edge cases in user data', () => {
const edgeCases = [
{ name: '', email: 'test@example.com', age: 18 }, // Empty name
{ name: 'Test', email: '', age: 18 }, // Empty email
{ name: 'Test', email: 'test@example.com', age: 0 }, // Boundary age
{ name: 'Test', email: 'test@example.com', age: null }, // Null age
];
edgeCases.forEach(user => {
expect(() => process(user)).toThrow();
});
});
"I'll write tests after to verify it works"
Tests written after code pass immediately. Passing immediately proves nothing:
Test-first forces you to see the test fail, proving it actually tests something.
"I already manually tested all the edge cases"
Manual testing is ad-hoc. You think you tested everything but:
Automated tests are systematic. They run the same way every time.
"Deleting X hours of work is wasteful"
Sunk cost fallacy. The time is already gone. Your choice now:
The "waste" is keeping code you can't trust. Working code without real tests is technical debt.
"TDD is dogmatic, being pragmatic means adapting"
TDD IS pragmatic:
"Pragmatic" shortcuts = debugging in production = slower.
"Tests after achieve the same goals - it's spirit not ritual"
No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"
Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones.
Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't).
30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work.
| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
All of these mean: Delete code. Start over with TDD.
Bug: Empty email accepted
RED
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
Verify RED
$ npm test
FAIL: expected 'Email required', got undefined
GREEN
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}
Verify GREEN
$ npm test
PASS
REFACTOR Extract validation for multiple fields if needed.
Before marking work complete:
Can't check all boxes? You skipped TDD. Start over.
| Problem | Solution |
|---|---|
| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
| Test too complicated | Design too complicated. Simplify interface. |
| Must mock everything | Code too coupled. Use dependency injection. |
| Test setup huge | Extract helpers. Still complex? Simplify design. |
Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
Never fix bugs without a test.
When adding mocks or test utilities, read testing-anti-patterns.md to avoid common pitfalls:
Production code → test exists and failed first
Otherwise → not TDD
No exceptions without your human partner's permission.