7.1 KiB
name, user-invocable, description
| name | user-invocable | description |
|---|---|---|
| tdd-minimalism | true | Use when: implementing code to pass tests, writing minimal implementations, TDD workflow. Enforces strict minimalism by analyzing tests first and generating only the absolute minimum code needed. Prevents overengineering and anticipatory code. Run this skill before implementation on any failing test. |
TDD Minimalism Guardrails
This skill enforces strict Test-Driven Development discipline by ensuring implementations contain only the code that tests require—nothing more.
Principle
Generate only the absolute minimum needed to pass tests. Anticipatory code, infrastructure for future scenarios, defensive checks, comments explaining future work, or over-engineered data structures all violate TDD and should be deleted before writing implementation code.
Pre-Implementation Workflow
Before writing implementation code, ALWAYS execute this checklist:
1. Analyze the Test
- Read the test completely
- Identify exactly what methods/functions the test calls
- Identify exactly what the test asserts
- Document nothing else matters for this test
2. Define Minimal State
Ask: "What data must persist across the test's method calls to satisfy the assertions?"
- Only that data gets stored as member variables
- Everything else is deleted before writing code
3. Implement Only Tested Paths
- Implement only the specific code paths the test exercises
- No conditional logic unless the test verifies multiple branches
- No utility methods or helper classes
- No defensive code (null checks, boundary conditions the test doesn't trigger)
- No comments explaining future features
- No includes you don't use
4. Question Every Line
For each piece of code, ask: "Does this test directly exercise this line?"
- No → Delete it immediately
- Yes → Keep it
Anti-Patterns to Eliminate
| ❌ Anti-Pattern | Why It's Wrong | ✅ Corrected |
|---|---|---|
| Store data in a map/vector when test uses only 1 item | Implements multi-item support not tested | Use single member variable |
Add if checks for untested scenarios |
Defensive code the test doesn't exercise | Return value directly |
Include #include <map> when using int |
Unused dependency | Remove it |
Define lastChoice member when select() doesn't read it |
Dead code | Remove unused members |
| Comment explaining "future work" or "TODO" | Out of scope for current test | No implementation comments |
Use compound assignment (+=, -=) when test calls method only once |
Anticipates future multi-call scenarios not covered by any test | Use simple assignment (=) instead |
| Create utility methods not called in test | Anticipatory code | Delete before commit |
Red-Green-Refactor Phases
Each phase must include running the test suite to confirm the expected outcome. The test command is ./run_tests.sh (or the project's equivalent).
Red Phase (Writing Tests)
-
Write a test describing one specific behavior
-
Run the test suite (
./run_tests.sh) — confirm the test fails (either fails to compile or fails at runtime on an assertion).- If the test passes: The behavior is already covered by existing code. STOP. Abort and discuss with the user. Options include: the test is redundant (delete it), the test name/assertions are wrong, or the requirement changed. Do NOT proceed without user input.
- If the test fails to compile: Write the absolute minimum stubs (empty classes, empty method signatures) needed to make the code compile, then run the test suite again — confirm the test now fails at runtime on the assertion (
EXPECT_*/ASSERT_*), not just on compilation. This validates the assertion logic is correct before implementing. - If the test fails at runtime: You may proceed to the Green phase.
-
Keep scope narrow (single scenario, not edge cases)
-
Test only what's currently required by the story
Green Phase (Implementation)
-
Implement only what the test directly calls
-
Use the simplest possible data types
-
Use the shortest possible logic
-
Run the test suite (
./run_tests.sh) — confirm it passes -
Reference this skill's anti-patterns table and eliminate violations before committing
-
Present the implementation to the user for review before committing or proceeding to the next test.
- Show the final state of all changed files (diff or summary)
- Ask explicitly: "Please review and approve before I proceed"
- Do NOT move to the next test, commit, or merge without explicit user approval
- If the user requests changes, make them and re-run the test suite before asking for review again
Refactor Phase
- Improve code clarity (rename variables, format nicely)
- Run the test suite (
./run_tests.sh) — confirm tests still pass - Never add functionality without a failing test first
- Never add infrastructure for "future" requirements
Post-Implementation Verification
After implementing and tests pass, run this verification:
- Can I remove any member variables? (Are they all accessed in at least one method?)
- Can I remove any
#includedirectives? (Do I use everything I include?) - Can I remove any conditional branches? (Does the test exercise all paths?)
- Can I remove any comments? (Are they implementation comments, not documentation?)
- Can I remove any method parameters? (Are they all used in the method?)
- Can I replace any compound assignment (
+=,-=, etc.) with simple assignment (=) without changing test outcomes?
If you answer yes to any question, make those deletions immediately. Do not proceed to the next test/feature without eliminating unnecessary code.
Example: The Vending Machine
Test (what it calls and asserts):
machine.addChoice(Choice::Cola, 0); // Calls this
machine.assignProduct(Choice::Cola, Brand::Coke); // Calls this
EXPECT_EQ(machine.select(Choice::Cola), Brand::Coke); // Asserts this
Minimal Implementation:
// Header: ONE member variable (lastBrand), because select() returns it
Brand lastBrand;
// Implementation
void addChoice(Choice, int) {} // Body empty; test calls but doesn't verify behavior
void assignProduct(Choice choice, Brand brand) { lastBrand = brand; }
std::optional<Brand> select(Choice choice) { return lastBrand; } // No if; test only selects once
Eliminated Code:
- ❌
#include <map>— Not using maps - ❌
std::map<Choice, Brand> productMap— Test uses only one assignment - ❌
if (it != productMap.end())— Test doesn't verify "choice not found" scenario - ❌ Comments explaining future work — Out of test scope
- ❌
Choice lastChoice—select()doesn't read it; never used