mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-11 04:34:56 +03:00
151 lines
5.3 KiB
Markdown
151 lines
5.3 KiB
Markdown
# TDD Decision Tree
|
|
|
|
Quick reference for deciding when and how to apply Test-Driven Development.
|
|
|
|
---
|
|
|
|
## Decision: Should I Use TDD Here?
|
|
|
|
```
|
|
Is this code...
|
|
│
|
|
├─ Business logic or data transformation?
|
|
│ └─ YES: Always TDD. No exceptions.
|
|
│
|
|
├─ An API endpoint (REST, GraphQL, RPC)?
|
|
│ └─ YES: Always TDD. Write request/response tests first.
|
|
│
|
|
├─ A bug fix?
|
|
│ └─ YES: Always TDD. Write a failing test that reproduces the bug first.
|
|
│
|
|
├─ A utility function or helper?
|
|
│ └─ YES: Always TDD. These are the easiest to TDD — pure input/output.
|
|
│
|
|
├─ A database query or repository method?
|
|
│ └─ YES: Always TDD. Test the query behavior, not the SQL syntax.
|
|
│
|
|
├─ A state machine or workflow?
|
|
│ └─ YES: Always TDD. Test each transition.
|
|
│
|
|
├─ UI layout or styling (CSS, Tailwind, visual positioning)?
|
|
│ └─ TDD optional. Visual output is hard to assert meaningfully.
|
|
│ Use snapshot tests or visual regression tools instead.
|
|
│
|
|
├─ Configuration or environment setup?
|
|
│ └─ TDD optional. Test that config loads correctly, but don't
|
|
│ TDD every config value. Integration tests are more useful.
|
|
│
|
|
├─ A database migration?
|
|
│ └─ TDD optional. Test that migration runs forward and backward.
|
|
│ Don't TDD the migration SQL itself.
|
|
│
|
|
├─ A prototype or spike?
|
|
│ └─ TDD optional. Spikes are throwaway. But if the spike becomes
|
|
│ real code, stop and add tests before continuing.
|
|
│
|
|
├─ Third-party integration glue code?
|
|
│ └─ TDD the contract, not the integration. Write tests against
|
|
│ the interface you expect, mock the external service.
|
|
│
|
|
└─ Generated code (scaffolding, boilerplate)?
|
|
└─ TDD optional. Test the generator if you wrote it.
|
|
Don't TDD the generated output.
|
|
```
|
|
|
|
---
|
|
|
|
## Decision Factors
|
|
|
|
When the tree above doesn't give a clear answer, weigh these factors:
|
|
|
|
| Factor | Favors TDD | Favors Test-After |
|
|
|--------|-----------|-------------------|
|
|
| **Testability** | Clear inputs/outputs, deterministic | Heavy side effects, UI rendering |
|
|
| **Complexity** | Multiple branches, edge cases | Straightforward single-path logic |
|
|
| **Risk** | Failure is costly (data loss, security) | Failure is cosmetic or low-impact |
|
|
| **Stability** | Requirements are clear and stable | Requirements are still changing |
|
|
| **Team convention** | Team expects TDD | Team doesn't practice TDD |
|
|
| **Confidence** | You're unsure how to implement it | You've built this exact thing before |
|
|
|
|
**Rule of thumb:** If you're unsure, use TDD. The cost of writing a test first is low. The cost of a bug in untested code is high.
|
|
|
|
---
|
|
|
|
## The TDD Cycle
|
|
|
|
```
|
|
1. RED — Write a failing test that defines the desired behavior
|
|
2. GREEN — Write the minimum code to make the test pass
|
|
3. REFACTOR — Clean up without changing behavior (tests still pass)
|
|
4. REPEAT — Next behavior
|
|
```
|
|
|
|
### Common Mistakes
|
|
|
|
- **Writing too much test at once** — Test one behavior per cycle
|
|
- **Writing implementation before the test fails** — The failing test is the spec
|
|
- **Skipping refactor** — Technical debt accumulates in GREEN if you don't clean up
|
|
- **Testing implementation details** — Test what it does, not how it does it
|
|
|
|
---
|
|
|
|
## Handling Legacy Code Without Tests
|
|
|
|
Legacy code (code without tests) requires a different entry point into TDD.
|
|
|
|
### Step 1: Characterization Tests
|
|
|
|
Before changing anything, write tests that capture current behavior:
|
|
|
|
```python
|
|
# Characterization test — documents what the code DOES, not what it SHOULD do
|
|
def test_calculate_total_current_behavior():
|
|
result = calculate_total(items=[{"price": 10, "qty": 2}])
|
|
assert result == 20 # Observed behavior, may or may not be correct
|
|
```
|
|
|
|
### Step 2: Identify the Change Boundary
|
|
|
|
What's the smallest piece of code you need to change? Draw a boundary around it.
|
|
|
|
### Step 3: Add Seams
|
|
|
|
If the code is untestable (hard dependencies, global state), add seams:
|
|
- Extract method
|
|
- Inject dependencies
|
|
- Wrap external calls
|
|
|
|
### Step 4: TDD the Change
|
|
|
|
Now that you have characterization tests protecting existing behavior and seams allowing isolation, use the normal RED-GREEN-REFACTOR cycle for your change.
|
|
|
|
### Step 5: Decide What to Keep
|
|
|
|
After the change, decide which characterization tests to keep:
|
|
- **Keep** tests that document important behavior
|
|
- **Replace** tests that covered the code you changed (your TDD tests are better)
|
|
- **Remove** tests that only existed to enable your refactoring
|
|
|
|
---
|
|
|
|
## TDD by Test Type
|
|
|
|
| Test Type | TDD Approach |
|
|
|-----------|-------------|
|
|
| **Unit tests** | Standard RED-GREEN-REFACTOR. One behavior per cycle. |
|
|
| **Integration tests** | Write the test against the integration boundary first. May need stubs for external services during RED phase. |
|
|
| **API tests** | Define the request and expected response first. Implement handler to make it pass. |
|
|
| **E2E tests** | Not typically TDD'd per-cycle. Write E2E tests for critical paths after unit/integration TDD. |
|
|
|
|
---
|
|
|
|
## Quick Checklist
|
|
|
|
Before claiming a task is done with TDD:
|
|
|
|
- [ ] Every production function has at least one test that was written before the function
|
|
- [ ] No test was written after the code it tests (except characterization tests for legacy code)
|
|
- [ ] All tests pass
|
|
- [ ] Code has been refactored after going GREEN
|
|
- [ ] Tests verify behavior, not implementation
|