From cee24bf043bb4875ec2f00a737bb2795c45322bd Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:50:59 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + plugin.lock.json | 80 ++ skills/tdd-methodology-expert/SKILL.md | 737 ++++++++++++++++++ .../assets/hook-templates/pre-commit.sh | 107 +++ .../hook-templates/user-prompt-submit.sh | 72 ++ skills/tdd-methodology-expert/plugin.json | 15 + .../references/code-smells.md | 532 +++++++++++++ .../references/tdd-principles.md | 391 ++++++++++ .../references/testing-patterns.md | 637 +++++++++++++++ .../scripts/check_tdd_compliance.py | 280 +++++++ .../scripts/setup_hooks.sh | 112 +++ .../scripts/validate_tests.py | 274 +++++++ 13 files changed, 3252 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/tdd-methodology-expert/SKILL.md create mode 100644 skills/tdd-methodology-expert/assets/hook-templates/pre-commit.sh create mode 100644 skills/tdd-methodology-expert/assets/hook-templates/user-prompt-submit.sh create mode 100644 skills/tdd-methodology-expert/plugin.json create mode 100644 skills/tdd-methodology-expert/references/code-smells.md create mode 100644 skills/tdd-methodology-expert/references/tdd-principles.md create mode 100644 skills/tdd-methodology-expert/references/testing-patterns.md create mode 100644 skills/tdd-methodology-expert/scripts/check_tdd_compliance.py create mode 100644 skills/tdd-methodology-expert/scripts/setup_hooks.sh create mode 100644 skills/tdd-methodology-expert/scripts/validate_tests.py diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..e79779f --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "tdd-methodology-expert", + "description": "Expertise in Test-Driven Development (TDD) methodology.", + "version": "0.0.0-2025.11.28", + "author": { + "name": "Tim Green", + "email": "rawveg@gmail.com" + }, + "skills": [ + "./skills/tdd-methodology-expert" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..22a6a47 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# tdd-methodology-expert + +Expertise in Test-Driven Development (TDD) methodology. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..45e1ca4 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,80 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:rawveg/skillsforge-marketplace:tdd-methodology-expert", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "5a52968801ef6f67fa7f60a832a03e6a0cb6fc10", + "treeHash": "6c19bef81538ec852423a0a81b62980d53ec46902db6fe5d3ed0a03d30ae6ce8", + "generatedAt": "2025-11-28T10:27:54.021480Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "tdd-methodology-expert", + "description": "Expertise in Test-Driven Development (TDD) methodology." + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "677a00493327372480673719b0fcde810a801f4a9659c97b3ec6130cc5a0da89" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "0b87debb6e81f308b064334a8091d9fb3050677603f137435c5e1d5ed6837749" + }, + { + "path": "skills/tdd-methodology-expert/plugin.json", + "sha256": "b07121a823cd01ab889c3481aea578214e0b74d160ede6098e3299d64170879a" + }, + { + "path": "skills/tdd-methodology-expert/SKILL.md", + "sha256": "b68ce2e134064a47cf1204867e230a98884261e7469eb6cb76c0e4458dc5a41b" + }, + { + "path": "skills/tdd-methodology-expert/references/code-smells.md", + "sha256": "336cbe45d1af325e0215b9719f3f8802ca1c9c1182fb72fb0c50cabb86951910" + }, + { + "path": "skills/tdd-methodology-expert/references/testing-patterns.md", + "sha256": "934464f20b033d3a8c1e31bf78c36a91395989d38eec71a1d84bb9f604541873" + }, + { + "path": "skills/tdd-methodology-expert/references/tdd-principles.md", + "sha256": "c4f62cc765bed8ffcab3c6955633e2c93efb01b26ab7185bcf08834afb56ccde" + }, + { + "path": "skills/tdd-methodology-expert/scripts/check_tdd_compliance.py", + "sha256": "e07c8d44aeae0ab21926dbb169846ce92b44aeee3f8e213d473b6874185736a6" + }, + { + "path": "skills/tdd-methodology-expert/scripts/setup_hooks.sh", + "sha256": "b1a06fd74b4fb489143113b8e6ec03f79ac0c3622510c6d0c5e1f3b0d726076a" + }, + { + "path": "skills/tdd-methodology-expert/scripts/validate_tests.py", + "sha256": "4537e2e35d50a06d9c4920740c6e67ed656d6069eb8bc8e961257e661fd405f6" + }, + { + "path": "skills/tdd-methodology-expert/assets/hook-templates/user-prompt-submit.sh", + "sha256": "1c0771eeaea8bee103459a1b328c5137fd72523547ef98757b278bcd3b484e3b" + }, + { + "path": "skills/tdd-methodology-expert/assets/hook-templates/pre-commit.sh", + "sha256": "ff24e43515d626aa65f1c87c327f45ec524a5ba9638083f1937e69fcd2eac159" + } + ], + "dirSha256": "6c19bef81538ec852423a0a81b62980d53ec46902db6fe5d3ed0a03d30ae6ce8" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/tdd-methodology-expert/SKILL.md b/skills/tdd-methodology-expert/SKILL.md new file mode 100644 index 0000000..e88d76a --- /dev/null +++ b/skills/tdd-methodology-expert/SKILL.md @@ -0,0 +1,737 @@ +--- +name: tdd-methodology-expert +description: Use proactively when you need to implement features or fix bugs using strict Test-Driven Development (TDD) methodology. This agent should be activated for any coding task that requires writing new functionality, refactoring existing code, or ensuring comprehensive test coverage, but should not be used for any design-related tasks. The agent excels at breaking down complex requirements into testable increments and maintaining high code quality through disciplined TDD cycles. Use this agent proactively or if the user mentions 'TDD', 'tdd' or 'Test Driven Development'. +--- + +# TDD Methodology Expert + +Enforce and reinforce Test-Driven Development (TDD) methodology throughout the software development process. This skill provides comprehensive guidance, automated validation, and continuous reinforcement of the Red-Green-Refactor cycle. + +## When to Use This Skill + +This skill automatically activates when: +- TDD is mentioned in `CLAUDE.md` or `CLAUDE.local.md` +- TDD is referenced in project memory +- The user explicitly requests TDD methodology +- The user asks to write tests or implement features + +Use this skill for: +- Implementing new features using TDD +- Refactoring existing code with test protection +- Fixing bugs with test-first approach +- Ensuring code quality through TDD discipline +- Validating that TDD principles are being followed + +Do NOT use this skill for: +- Pure design discussions without implementation +- Documentation-only tasks +- Research or exploration tasks + +## Core TDD Methodology + +Test-Driven Development follows a strict three-phase cycle that must be repeated for every increment of functionality: + +### 🔴 Red Phase: Write a Failing Test + +**Always write the test before any production code.** + +1. **Write a test** that expresses the desired behavior +2. **Run the test** and verify it fails (for the right reason) +3. **Confirm** the failure message indicates what's missing + +**Red Phase Principles**: +- Test must be simple and focused on one behavior +- Test name should clearly describe expected behavior +- Test should be readable without looking at implementation +- Failure should be meaningful and guide implementation + +**Example Flow**: +``` +1. Write: test_should_calculate_total_with_tax() +2. Run: Test fails - "ShoppingCart has no attribute 'calculate_total'" +3. ✅ Ready for Green phase +``` + +### 🟢 Green Phase: Make the Test Pass + +**Write minimal code to make the failing test pass.** + +1. **Implement** the simplest code that makes the test pass +2. **Run the test** and verify it passes +3. **Run all tests** to ensure nothing broke + +**Green Phase Principles**: +- Write only enough code to pass the current test +- Don't add features not required by tests +- It's okay to use shortcuts (you'll refactor later) +- Focus on making it work, not making it perfect + +**Example Flow**: +``` +1. Implement: def calculate_total(self, tax_rate): ... +2. Run: Test passes ✅ +3. Run all: All tests pass ✅ +4. ✅ Ready for Refactor phase +``` + +### 🔵 Refactor Phase: Improve the Code + +**Clean up code while maintaining passing tests.** + +1. **Identify** duplication, poor names, or structural issues +2. **Refactor** incrementally, running tests after each change +3. **Verify** all tests still pass +4. **Repeat** until code is clean + +**Refactor Phase Principles**: +- Never refactor with failing tests +- Make small, safe changes +- Run tests after each refactoring step +- Improve both production code and test code +- Apply design patterns and best practices + +**Example Flow**: +``` +1. Identify: Duplicated tax calculation logic +2. Extract: Move to _calculate_tax() method +3. Run tests: All pass ✅ +4. Improve: Better variable names +5. Run tests: All pass ✅ +6. ✅ Commit and move to next feature +``` + +## TDD Workflow Integration + +### Before Starting Any Code Task + +**Step 1: Understand the Requirement** +- Break down the task into small, testable behaviors +- Identify the simplest test case to start with +- State which TDD phase you're entering (Red) + +**Step 2: Plan the Test** +- Describe what test you're about to write +- Explain what behavior it will verify +- Confirm test will fail before implementation + +### During Implementation + +**Always follow this sequence**: + +1. **🔴 Red**: Write failing test → Run → Verify failure +2. **🟢 Green**: Write minimal code → Run → Verify pass +3. **🔵 Refactor**: Improve code → Run → Verify still passes +4. **Commit**: Save working, tested, clean code +5. **Repeat**: Next test for next behavior + +**Never skip phases or reverse the order.** + +### Communicating TDD Progress + +In every response involving code changes, explicitly state: +- **Current phase**: Which phase you're in (Red/Green/Refactor) +- **Test status**: Whether tests are passing or failing +- **Next steps**: What comes next in the cycle + +**Example Communication**: +``` +🔴 RED PHASE: Writing a test for calculating order total with discounts. + +Test: test_should_apply_percentage_discount_to_order_total() +Expected to fail because Order.apply_discount() doesn't exist yet. + +[Test code here] + +Running test... ❌ Fails as expected: "Order has no attribute 'apply_discount'" + +🟢 GREEN PHASE: Implementing minimal code to pass the test... + +[Implementation code here] + +Running test... ✅ Passes! +Running all tests... ✅ All pass! + +🔵 REFACTOR PHASE: Improving the discount calculation structure... + +[Refactored code here] + +Running all tests... ✅ All pass! +Ready to commit this increment. +``` + +## Bundled Tools and Resources + +### Scripts + +#### check_tdd_compliance.py + +Analyzes code to detect TDD compliance issues and code smells that indicate test-after development. + +**Usage**: +```bash +python scripts/check_tdd_compliance.py +``` + +**What it checks**: +- Nested conditionals (sign of poor TDD structure) +- Long methods (TDD produces small, focused methods) +- Complex boolean conditions (TDD encourages extraction) +- Missing abstractions (type checking vs polymorphism) +- Test coverage (presence of corresponding test files) + +**When to use**: +- After completing a feature or module +- Before committing code +- When reviewing code quality +- During refactoring sessions + +#### validate_tests.py + +Validates that tests exist, are properly structured, and follow TDD patterns. + +**Usage**: +```bash +python scripts/validate_tests.py +``` + +**What it checks**: +- Test file existence and structure +- Test case count and naming +- Arrange-Act-Assert pattern adherence +- Test size and complexity +- Descriptive test names + +**When to use**: +- Before committing new tests +- When validating test quality +- During code review +- After writing a batch of tests + +#### setup_hooks.sh + +Installs git hooks and Claude Code hooks to enforce TDD methodology automatically. + +**Usage**: +```bash +bash scripts/setup_hooks.sh +``` + +**What it installs**: +- Git pre-commit hook: Validates TDD compliance before commits +- Claude user-prompt-submit hook: Injects TDD reminders into every interaction +- Updates CLAUDE.md to document TDD requirement + +**When to use**: +- Once at project initialization +- When onboarding new team members to TDD +- When setting up TDD enforcement for the first time + +### References + +Load these references when deeper understanding is needed: + +#### tdd-principles.md + +Comprehensive guide to TDD methodology including: +- The Red-Green-Refactor cycle in detail +- TDD philosophy and benefits +- Best practices and common mistakes +- TDD in different contexts (unit, integration, acceptance) +- Measuring TDD effectiveness + +**When to reference**: When explaining TDD concepts or resolving questions about methodology. + +#### code-smells.md + +Catalog of code smells that indicate test-after development: +- High-severity smells (nested conditionals, long methods, god objects) +- Medium-severity smells (type checking, duplication, primitive obsession) +- Low-severity smells (magic numbers, long parameter lists) +- Detection strategies and refactoring guidance + +**When to reference**: When analyzing code quality or identifying non-TDD patterns. + +**Grep patterns for searching**: +- Nested conditionals: `if.*:\s*\n\s+if` +- Long methods: Count lines between function definitions +- Type checking: `isinstance\(|typeof ` +- God classes: Count methods per class + +#### testing-patterns.md + +Language-agnostic testing patterns and best practices: +- Test structure patterns (AAA, Given-When-Then) +- Test organization (fixtures, builders, object mothers) +- Assertion patterns +- Test doubles (stubs, mocks, fakes) +- Parameterized testing +- Exception testing +- Test naming conventions + +**When to reference**: When writing tests or improving test structure. + +### Assets + +#### Hook Templates + +Located in `assets/hook-templates/`: + +- **pre-commit.sh**: Git hook that runs TDD compliance checks before allowing commits +- **user-prompt-submit.sh**: Claude Code hook that injects TDD reminders before every user prompt + +These templates are used by `setup_hooks.sh` and can be customized for specific project needs. + +## Prompt-Based Validation + +Users can invoke TDD validation scripts through natural language prompts instead of relying on git hooks. This provides flexibility for developers who prefer manual commits and on-demand validation. + +### Recognizing Validation Requests + +When the user requests TDD validation using phrases like: +- "Check the code for TDD compliance" +- "Validate TDD adherence" +- "Run TDD compliance check" +- "Check if this code follows TDD" +- "Analyze code for TDD smells" +- "Check code quality" +- "Run code smell detection" + +**Immediately respond by**: + +1. **Identify target code**: Determine which files or directories to analyze + - If user specifies files/directories, use those + - If not specified, ask for clarification or analyze recently modified files + - Use `git diff --name-only` to find recently changed files if appropriate + +2. **Run compliance check**: Execute `check_tdd_compliance.py` on the target code + ```bash + python scripts/check_tdd_compliance.py + ``` + +3. **Parse and present results**: + - Report the TDD compliance score (0-100) + - List any code smells found with severity levels + - Highlight specific issues in the code + - Provide actionable recommendations for improvement + +4. **Validate tests** (if applicable): Run `validate_tests.py` on corresponding test files + ```bash + python scripts/validate_tests.py + ``` + +5. **Provide context-aware guidance**: + - Reference specific code smells from `references/code-smells.md` + - Suggest refactoring patterns from `references/testing-patterns.md` + - Offer to refactor problematic code sections + +### Example Response Flow + +**User**: "Check the code for TDD compliance" + +**Claude Response**: +``` +I'll analyze the code for TDD compliance. Let me identify the recently changed files and run the compliance check. + +[Runs check_tdd_compliance.py on relevant files] + +📊 TDD COMPLIANCE REPORT + +File: src/order_processor.py +TDD Score: 65/100 (Needs Improvement) + +Issues Found: +❌ High Severity (3 issues): + - Line 45-68: Deeply nested conditionals (4 levels deep) + - Line 112-145: Long method 'process_order' (34 lines) + - Line 78-82: Complex boolean condition (5 logical operators) + +⚠️ Medium Severity (2 issues): + - Line 156: Type checking instead of polymorphism (isinstance) + - Line 203-215: Duplicate code block + +Recommendations: +1. Extract nested conditionals into separate validation methods +2. Break down 'process_order' into smaller, focused methods +3. Extract complex boolean into named predicate method +4. Use polymorphism instead of type checking +5. Extract duplicate logic into reusable function + +Would you like me to refactor any of these issues? +``` + +### When NOT to Run Validation + +Do not automatically run validation scripts unless: +- User explicitly requests validation/compliance check +- User is about to commit and asks for pre-commit checks +- User asks about code quality or TDD adherence + +Avoid running validation: +- During normal TDD cycles (Red-Green-Refactor) +- When writing initial test code +- During exploratory coding or prototyping +- When user hasn't requested it + +### Manual vs. Automated Validation + +**Manual (Prompt-Based)**: +- User has full control over when checks run +- Can validate specific files or directories +- Immediate feedback and context-aware guidance +- No interruption to commit workflow +- Preferred for developers who commit manually + +**Automated (Git Hooks)**: +- Runs automatically before commits (if hooks installed) +- Ensures validation happens consistently +- Blocks commits with poor TDD compliance +- Can be bypassed with `--no-verify` flag +- Optional setup via `setup_hooks.sh` + +Both approaches are valid. Users can choose the workflow that fits their preferences. + +## Prompt-Based Setup + +Users can request TDD setup for their project through natural language prompts. This provides a guided setup experience that respects user preferences. + +### Recognizing Setup Requests + +When the user requests TDD setup using phrases like: +- "Setup this project for TDD" +- "Configure TDD for this project" +- "Initialize TDD development" +- "Set up TDD methodology" +- "Enable TDD for this codebase" +- "Install TDD tools" + +**Immediately respond by**: + +1. **Confirm the request**: Acknowledge that you'll set up TDD for the project + +2. **Update CLAUDE.md**: Always add or update the TDD requirement + ```markdown + # Development Guidelines + + ## Test-Driven Development (TDD) + + This project follows strict Test-Driven Development methodology: + - Write tests before production code (Red-Green-Refactor cycle) + - All features must have corresponding unit tests + - Code quality is validated through TDD compliance checks + + Use the `tdd-methodology-expert` skill for TDD guidance and validation. + ``` + +3. **Install Claude Code hook**: Always install the user-prompt-submit hook + - Copy `assets/hook-templates/user-prompt-submit.sh` to `.claude/hooks/user-prompt-submit` + - Make executable: `chmod +x .claude/hooks/user-prompt-submit` + - This injects TDD reminders into every conversation + +4. **Offer pre-commit hook (optional)**: Proactively ask about git hook installation + + **Prompt the user**: + ``` + ✅ TDD Setup Progress: + - ✓ Updated CLAUDE.md with TDD requirements + - ✓ Installed Claude Code hook for TDD reinforcement + + 📋 Optional Enhancement: + + Would you like to install the git pre-commit hook for automated TDD compliance checking? + + Benefits: + ✅ Automatically validates TDD compliance before each commit + ✅ Prevents commits with poor code quality (blocking enforcement) + ✅ Provides immediate feedback on code smells + + Trade-offs: + ⚠️ Adds ~2-5 seconds to commit time + ⚠️ Blocks commits that fail TDD compliance (can bypass with --no-verify) + ⚠️ Some developers prefer manual validation + + You can always validate manually by saying "Check the code for TDD compliance" + + Install pre-commit hook? (yes/no) + ``` + +5. **Install pre-commit hook (if user agrees)**: + - Copy `assets/hook-templates/pre-commit.sh` to `.git/hooks/pre-commit` + - Make executable: `chmod +x .git/hooks/pre-commit` + - Confirm installation + +6. **Provide completion summary**: + ``` + ✅ TDD Setup Complete! + + What was installed: + - ✓ CLAUDE.md updated with TDD requirements + - ✓ Claude Code hook (user-prompt-submit) + - ✓ Git pre-commit hook (if user opted in) + + Available validation scripts: + - check_tdd_compliance.py - Analyze code for TDD compliance + - validate_tests.py - Validate test quality and structure + + How to use: + - Say "Check the code for TDD compliance" anytime + - All code development will follow Red-Green-Refactor cycle + - I'll provide TDD guidance and validation throughout + + Ready to start TDD development! + ``` + +### Example Setup Flow + +**User**: "Please setup this project for TDD development" + +**Claude**: +``` +I'll set up TDD for your project. Let me configure the necessary files and hooks. + +[Updates CLAUDE.md with TDD requirements] +[Installs Claude Code hook] + +✅ TDD Setup Progress: +- ✓ Updated CLAUDE.md with TDD requirements +- ✓ Installed Claude Code hook for TDD reinforcement + +📋 Optional Enhancement: + +Would you like to install the git pre-commit hook for automated TDD compliance checking? + +Benefits: +✅ Automatically validates TDD compliance before each commit +✅ Prevents commits with poor code quality +✅ Provides immediate feedback on code smells + +Trade-offs: +⚠️ Adds ~2-5 seconds to commit time +⚠️ Blocks commits that fail TDD compliance (bypass with --no-verify) +⚠️ Some developers prefer manual validation + +You can always validate manually by saying "Check the code for TDD compliance" + +Install pre-commit hook? (yes/no) +``` + +**User**: "no thanks, I prefer manual validation" + +**Claude**: +``` +✅ TDD Setup Complete! + +What was installed: +- ✓ CLAUDE.md updated with TDD requirements +- ✓ Claude Code hook (user-prompt-submit) +- ✗ Git pre-commit hook (skipped per user preference) + +Available validation scripts: +- check_tdd_compliance.py - Analyze code for TDD compliance +- validate_tests.py - Validate test quality and structure + +How to use: +- Say "Check the code for TDD compliance" anytime for manual validation +- All code development will follow Red-Green-Refactor cycle +- I'll provide TDD guidance throughout + +Ready to start TDD development! +``` + +### Setup Components + +**Always Install**: +- CLAUDE.md update (TDD documentation) +- Claude Code user-prompt-submit hook (TDD reminders) + +**Optional Install** (ask user): +- Git pre-commit hook (automated validation) + +**Never Install Without Asking**: +- Git hooks that modify user's commit workflow + +### Handling Existing Setup + +If TDD is already configured: +- Check if CLAUDE.md mentions TDD +- Check if `.claude/hooks/user-prompt-submit` exists +- Check if `.git/hooks/pre-commit` exists +- Report current status and offer to update or reinstall + +## TDD Enforcement Rules + +### Mandatory Requirements + +1. **Tests must be written before production code** + - Never implement features without failing tests first + - Never fix bugs without reproducing them in a test first + +2. **All tests must pass before committing** + - Red phase: Don't commit + - Green phase: Can commit, but prefer to refactor first + - Refactor phase: Commit here with clean, tested code + +3. **Refactoring is not optional** + - Every cycle must include the refactor phase + - Code quality must continuously improve + - Technical debt must be addressed immediately + +4. **Tests must follow TDD patterns** + - Descriptive, behavior-focused names + - Arrange-Act-Assert structure + - Single responsibility per test + - Independent and isolated + +### Validation and Feedback + +**Continuously validate TDD adherence by**: + +1. **Checking code structure**: + - Run `check_tdd_compliance.py` on changed files + - Report TDD score and any code smells found + - Provide specific guidance on improvements + +2. **Verifying test quality**: + - Run `validate_tests.py` on test files + - Ensure tests follow patterns from `testing-patterns.md` + - Check for descriptive names and proper structure + +3. **Monitoring the cycle**: + - Confirm each phase is completed in order + - Verify tests fail before implementation + - Ensure tests pass after implementation + - Validate refactoring maintains passing tests + +### User Reassurance + +Throughout the TDD process, continuously reassure the user that: + +- ✅ TDD methodology is being strictly followed +- ✅ Red-Green-Refactor cycle is being respected +- ✅ Tests are written before code +- ✅ Code quality is being maintained +- ✅ Each phase is completed properly + +**Example Reassurance**: +``` +✅ TDD COMPLIANCE: Following strict Red-Green-Refactor methodology. + +Current status: +- 🔴 Red: Test written and confirmed failing +- 🟢 Green: Implementation passes test +- 🔵 Refactor: Code cleaned up, all tests pass +- TDD Score: 95/100 (Excellent) + +The code exhibits TDD characteristics: +✓ Small, focused functions (8-12 lines) +✓ Flat control flow (no deep nesting) +✓ Clear separation of concerns +✓ High testability + +Ready to proceed with next feature. +``` + +## Common TDD Scenarios + +### Implementing a New Feature + +**Process**: +1. Break feature into small, testable behaviors +2. For each behavior: + - 🔴 Write failing test + - 🟢 Implement minimal code + - 🔵 Refactor for quality +3. Validate with `check_tdd_compliance.py` +4. Commit clean, tested code + +### Fixing a Bug + +**Process**: +1. 🔴 Write a test that reproduces the bug (should fail) +2. Confirm test fails with current code +3. 🟢 Fix the bug (test should now pass) +4. 🔵 Refactor if needed +5. Validate no new code smells introduced +6. Commit fix with test + +### Refactoring Existing Code + +**Process**: +1. Ensure existing tests pass (or add characterization tests) +2. For each refactoring increment: + - 🔵 Make small structural improvement + - Run tests to verify behavior unchanged + - Repeat until code is clean +3. Validate improvement with compliance check +4. Commit refactored code + +### Adding Tests to Legacy Code + +**Process**: +1. Identify functionality to test +2. Write characterization tests (document current behavior) +3. Use tests to enable safe refactoring +4. Gradually improve structure following TDD +5. Future changes follow strict TDD + +## Integration with Development Workflow + +### IDE Integration + +Tests should be: +- Easy to run (one command or hotkey) +- Fast to execute (milliseconds for unit tests) +- Clearly reported (pass/fail with details) + +### Continuous Integration + +Every commit should: +- Run all tests automatically +- Report TDD compliance metrics +- Block merge if tests fail +- Track code quality trends + +### Code Review + +Reviewers should verify: +- Tests exist for all changes +- Tests follow TDD patterns +- Code exhibits TDD structure (small, flat, clean) +- Red-Green-Refactor was followed + +## Troubleshooting TDD Issues + +### "Tests are taking too long to write" + +**Solution**: Tests are probably too big. Break into smaller behaviors. + +### "Can't figure out what to test first" + +**Solution**: Test the simplest case. Start with happy path, then edge cases. + +### "Tests keep breaking during refactoring" + +**Solution**: Tests are testing implementation, not behavior. Refactor tests too. + +### "Code is getting messy" + +**Solution**: Not refactoring enough. Spend more time in refactor phase. + +### "Tests are hard to write" + +**Solution**: Code design is poor. Let test difficulty guide design improvements. + +## Summary: TDD Mindset + +When using this skill, always remember: + +1. **Tests First**: Never write production code without a failing test +2. **Small Steps**: Keep Red-Green-Refactor cycles very short (2-10 minutes) +3. **Refactor Always**: Code quality is not optional +4. **Trust the Process**: TDD produces better design through discipline +5. **Verify Compliance**: Use bundled tools to validate adherence + +**The cycle is sacred: 🔴 Red → 🟢 Green → 🔵 Refactor** + +Never skip phases. Never reverse the order. Always complete the cycle. + +TDD is not just about testing - it's a design methodology that produces higher-quality, more maintainable code through disciplined practice. diff --git a/skills/tdd-methodology-expert/assets/hook-templates/pre-commit.sh b/skills/tdd-methodology-expert/assets/hook-templates/pre-commit.sh new file mode 100644 index 0000000..1eb8b38 --- /dev/null +++ b/skills/tdd-methodology-expert/assets/hook-templates/pre-commit.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# +# Git Pre-Commit Hook for TDD Enforcement +# +# This hook runs before each git commit to ensure TDD principles are followed. +# It validates that tests exist and checks for code smells that indicate +# tests-after-code development. + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}🔍 Running TDD compliance check...${NC}" + +# Find the skill directory (assuming it's in a standard location) +# You may need to adjust this path based on where the skill is installed +SKILL_SCRIPT="" +if [ -f "$HOME/.claude/skills/tdd-methodology-expert/scripts/check_tdd_compliance.py" ]; then + SKILL_SCRIPT="$HOME/.claude/skills/tdd-methodology-expert/scripts/check_tdd_compliance.py" +elif [ -f "./.claude/skills/tdd-methodology-expert/scripts/check_tdd_compliance.py" ]; then + SKILL_SCRIPT="./.claude/skills/tdd-methodology-expert/scripts/check_tdd_compliance.py" +fi + +if [ -z "$SKILL_SCRIPT" ]; then + echo -e "${YELLOW}⚠️ TDD compliance script not found. Skipping check.${NC}" + exit 0 +fi + +# Get list of staged files +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM) + +if [ -z "$STAGED_FILES" ]; then + echo -e "${GREEN}✅ No files to check${NC}" + exit 0 +fi + +# Create temporary directory for checking +TEMP_DIR=$(mktemp -d) +trap "rm -rf $TEMP_DIR" EXIT + +# Copy staged files to temp directory +for FILE in $STAGED_FILES; do + if [ -f "$FILE" ]; then + mkdir -p "$TEMP_DIR/$(dirname "$FILE")" + git show ":$FILE" > "$TEMP_DIR/$FILE" + fi +done + +# Run TDD compliance check on staged files +echo -e "${BLUE}Analyzing staged files for TDD compliance...${NC}" +OUTPUT=$(python3 "$SKILL_SCRIPT" "$TEMP_DIR" 2>&1) +EXIT_CODE=$? + +# Parse results +COMPLIANCE_LEVEL=$(echo "$OUTPUT" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['compliance'])" 2>/dev/null || echo "unknown") +TDD_SCORE=$(echo "$OUTPUT" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['metrics']['tdd_score'])" 2>/dev/null || echo "0") +CODE_SMELLS=$(echo "$OUTPUT" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['metrics']['code_smells'])" 2>/dev/null || echo "0") + +echo "" +echo "📊 TDD Compliance Report:" +echo " Score: $TDD_SCORE/100" +echo " Level: $COMPLIANCE_LEVEL" +echo " Code Smells: $CODE_SMELLS" +echo "" + +if [ "$EXIT_CODE" -ne 0 ]; then + echo -e "${RED}❌ TDD compliance check failed!${NC}" + echo "" + echo "Issues found:" + echo "$OUTPUT" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + for issue in data['issues'][:5]: # Show first 5 issues + severity = issue.get('severity', 'unknown') + msg = issue.get('message', 'Unknown issue') + file = issue.get('file', '') + line = issue.get('line', '') + + location = f'{file}:{line}' if line else file + print(f' • [{severity.upper()}] {location}') + print(f' {msg}') + print() +except: + pass +" 2>/dev/null + + echo "" + echo -e "${YELLOW}💡 TDD Reminder:${NC}" + echo " 1. 🔴 Write a failing test first" + echo " 2. 🟢 Write minimal code to pass the test" + echo " 3. 🔵 Refactor while keeping tests green" + echo "" + echo "To commit anyway, use: git commit --no-verify" + echo "" + + exit 1 +fi + +echo -e "${GREEN}✅ TDD compliance check passed!${NC}" +echo "" +exit 0 diff --git a/skills/tdd-methodology-expert/assets/hook-templates/user-prompt-submit.sh b/skills/tdd-methodology-expert/assets/hook-templates/user-prompt-submit.sh new file mode 100644 index 0000000..ed39445 --- /dev/null +++ b/skills/tdd-methodology-expert/assets/hook-templates/user-prompt-submit.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Claude Code User-Prompt-Submit Hook for TDD Reinforcement +# +# This hook runs before every user prompt is submitted to Claude, +# injecting TDD methodology reminders and context into the conversation. + +# Colors for output (for debugging) +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Get the user's original prompt from stdin +USER_PROMPT=$(cat) + +# Build TDD reinforcement message +TDD_REMINDER=" + +🔴🟢🔵 TDD Methodology Reminder: + +This project follows Test-Driven Development (TDD). Every code change must follow the Red-Green-Refactor cycle: + +1. 🔴 RED Phase: Write a failing test first + - Write the test before any production code + - Test should fail because the feature doesn't exist yet + - Test should clearly express the desired behavior + +2. 🟢 GREEN Phase: Make the test pass + - Write the minimal code needed to pass the test + - Don't add extra features or abstractions yet + - Focus on making the test pass as quickly as possible + +3. 🔵 REFACTOR Phase: Improve the code + - Clean up the code while keeping tests green + - Remove duplication + - Improve names and structure + - Ensure code is maintainable and readable + +Code developed with TDD exhibits these characteristics: +✅ Small, focused functions/methods (typically <20 lines) +✅ Flat control flow (minimal nesting, no deep if/elif/else chains) +✅ Clear separation of concerns +✅ High testability and mockability +✅ Simple, straightforward logic + +Code developed test-after (NOT TDD) often shows: +❌ Long, complex methods +❌ Deeply nested conditionals +❌ Complex boolean expressions +❌ Type checking instead of polymorphism +❌ Tight coupling and poor abstractions + +When responding to this request: +• Explicitly state which TDD phase you're in (Red, Green, or Refactor) +• Write tests BEFORE any production code +• Verify tests fail before implementing features (Red phase) +• Implement minimal code to pass tests (Green phase) +• Refactor only after tests are passing (Refactor phase) +• Validate TDD compliance using the check_tdd_compliance.py script when appropriate +• Reassure the user that TDD principles are being followed + +User's original request: +$USER_PROMPT +" + +# Output the augmented prompt +echo "$TDD_REMINDER" + +# Exit successfully +exit 0 diff --git a/skills/tdd-methodology-expert/plugin.json b/skills/tdd-methodology-expert/plugin.json new file mode 100644 index 0000000..bb9e4fa --- /dev/null +++ b/skills/tdd-methodology-expert/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "tdd-methodology-expert", + "description": "Provides TDD methodology expert integration for Claude Code.", + "version": "1.0.0", + "author": { + "name": "Tim Green", + "email": "rawveg@gmail.com" + }, + "homepage": "https://github.com/rawveg/claude-skills-marketplace", + "repository": "https://github.com/rawveg/claude-skills-marketplace", + "license": "MIT", + "keywords": ["tdd", "tdd-methodology-expert", "coding standards", "code quality", "Claude Code"], + "category": "productivity", + "strict": false +} diff --git a/skills/tdd-methodology-expert/references/code-smells.md b/skills/tdd-methodology-expert/references/code-smells.md new file mode 100644 index 0000000..ab03dbf --- /dev/null +++ b/skills/tdd-methodology-expert/references/code-smells.md @@ -0,0 +1,532 @@ +# Code Smells That Indicate Test-After Development + +This document catalogs code smells and anti-patterns that strongly suggest tests were written after implementation rather than following TDD methodology. + +## Understanding Code Smells in TDD Context + +When developers write tests after code (test-after), they tend to produce different code structures than when following TDD. This is because: + +1. **TDD enforces small steps**: Each test drives minimal implementation +2. **TDD encourages refactoring**: The refactor phase continuously improves structure +3. **TDD requires testability**: Code must be designed for easy testing from the start +4. **TDD prevents over-engineering**: Only write code needed to pass tests + +Test-after code often shows signs of: +- Solving problems that don't exist yet (premature optimization) +- Complex structures built all at once (big bang implementation) +- Difficult-to-test designs (retrofitted testability) +- Accumulated technical debt (skipped refactoring) + +## High-Severity Code Smells + +### 1. Deeply Nested Conditionals + +**Description**: Multiple levels of if/elif/else statements nested within each other. + +**Why it indicates test-after**: +- TDD would break this down into separate, testable functions +- Each branch would have its own test, encouraging extraction +- Refactor phase would identify and eliminate deep nesting + +**Example (Bad)**: +```python +def process_order(order): + if order.customer: + if order.customer.is_active: + if order.items: + if order.total > 0: + if order.payment_method: + if order.payment_method == "credit_card": + if order.customer.credit_limit >= order.total: + # Process credit card payment + return "processed" + else: + return "insufficient_credit" + else: + # Process other payment + return "processed" + else: + return "no_payment_method" + else: + return "invalid_total" + else: + return "no_items" + else: + return "inactive_customer" + else: + return "no_customer" +``` + +**TDD Alternative**: +```python +def process_order(order): + _validate_order(order) + _validate_customer(order.customer) + _validate_payment(order) + return _execute_payment(order) + +def _validate_order(order): + if not order.items: + raise OrderValidationError("Order must have items") + if order.total <= 0: + raise OrderValidationError("Order total must be positive") + +def _validate_customer(customer): + if not customer: + raise OrderValidationError("Order must have a customer") + if not customer.is_active: + raise OrderValidationError("Customer is inactive") + +def _validate_payment(order): + if not order.payment_method: + raise OrderValidationError("Payment method required") + +def _execute_payment(order): + payment_processor = PaymentProcessorFactory.create(order.payment_method) + return payment_processor.process(order) +``` + +**How to detect**: Look for 3+ levels of nested if/else statements. + +### 2. Long Methods/Functions + +**Description**: Methods exceeding 20-30 lines of code. + +**Why it indicates test-after**: +- TDD naturally produces small functions (5-15 lines) +- Each test typically drives one small piece of functionality +- Long methods suggest big-bang implementation + +**Example (Bad)**: +```python +def generate_invoice(order_id): + # 80+ lines of mixed responsibilities: + # - Database queries + # - Business logic + # - Calculations + # - Formatting + # - File generation + # - Email sending + order = db.query(Order).filter_by(id=order_id).first() + if not order: + return None + + total = 0 + for item in order.items: + if item.discount: + price = item.price * (1 - item.discount) + else: + price = item.price + total += price * item.quantity + + tax = total * 0.1 + shipping = 10 if total < 50 else 0 + grand_total = total + tax + shipping + + # ... 50 more lines of formatting and sending +``` + +**TDD Alternative**: +```python +def generate_invoice(order_id): + order = _fetch_order(order_id) + invoice_data = _calculate_invoice_totals(order) + formatted_invoice = _format_invoice(order, invoice_data) + _send_invoice(order.customer.email, formatted_invoice) + return formatted_invoice + +def _fetch_order(order_id): + order = db.query(Order).filter_by(id=order_id).first() + if not order: + raise OrderNotFoundError(f"Order {order_id} not found") + return order + +def _calculate_invoice_totals(order): + subtotal = sum(_calculate_line_total(item) for item in order.items) + tax = _calculate_tax(subtotal) + shipping = _calculate_shipping(subtotal) + return InvoiceTotals(subtotal, tax, shipping) + +def _calculate_line_total(item): + price = item.price * (1 - item.discount) if item.discount else item.price + return price * item.quantity +``` + +**How to detect**: Count lines in methods. Flag anything over 20 lines. + +### 3. Complex Boolean Conditions + +**Description**: Conditional expressions with multiple AND/OR operators. + +**Why it indicates test-after**: +- TDD encourages extracting complex conditions into named methods +- Each condition part would have its own test +- Refactor phase would identify complexity and extract it + +**Example (Bad)**: +```python +if (user.age >= 18 and user.has_license and + user.years_experience >= 2 and + (user.state == "CA" or user.state == "NY") and + not user.has_violations and user.insurance_valid): + # Allow to rent car + pass +``` + +**TDD Alternative**: +```python +def can_rent_car(user): + return (is_eligible_driver(user) and + is_in_service_area(user) and + has_clean_record(user)) + +def is_eligible_driver(user): + return user.age >= 18 and user.has_license and user.years_experience >= 2 + +def is_in_service_area(user): + return user.state in ["CA", "NY"] + +def has_clean_record(user): + return not user.has_violations and user.insurance_valid +``` + +**How to detect**: Count AND/OR operators. Flag conditions with 3+ logical operators. + +### 4. God Objects/Classes + +**Description**: Classes with too many responsibilities and methods. + +**Why it indicates test-after**: +- TDD enforces Single Responsibility Principle through testing +- Each test focuses on one behavior, encouraging focused classes +- Testing god objects is painful, encouraging decomposition + +**Example (Bad)**: +```python +class UserManager: + def authenticate(self, username, password): pass + def create_user(self, user_data): pass + def update_user(self, user_id, data): pass + def delete_user(self, user_id): pass + def send_welcome_email(self, user): pass + def send_password_reset(self, user): pass + def validate_email(self, email): pass + def validate_password(self, password): pass + def log_user_activity(self, user, action): pass + def generate_report(self, user_id): pass + def export_user_data(self, user_id): pass + def import_users(self, file_path): pass + # ... 20 more methods +``` + +**TDD Alternative**: +```python +class AuthenticationService: + def authenticate(self, username, password): pass + +class UserRepository: + def create(self, user): pass + def update(self, user_id, data): pass + def delete(self, user_id): pass + def find_by_id(self, user_id): pass + +class EmailService: + def send_welcome_email(self, user): pass + def send_password_reset(self, user): pass + +class UserValidator: + def validate_email(self, email): pass + def validate_password(self, password): pass + +class UserReportGenerator: + def generate_report(self, user_id): pass +``` + +**How to detect**: Count methods in class. Flag classes with 10+ methods. + +## Medium-Severity Code Smells + +### 5. Type Checking Instead of Polymorphism + +**Description**: Using `isinstance()`, `typeof`, or type switches instead of polymorphic design. + +**Why it indicates test-after**: +- TDD encourages interface-based design through mocking +- Polymorphism emerges naturally when testing behaviors +- Type checking makes testing harder, encouraging better design + +**Example (Bad)**: +```python +def calculate_area(shape): + if isinstance(shape, Circle): + return 3.14159 * shape.radius ** 2 + elif isinstance(shape, Rectangle): + return shape.width * shape.height + elif isinstance(shape, Triangle): + return 0.5 * shape.base * shape.height + else: + raise ValueError("Unknown shape type") +``` + +**TDD Alternative**: +```python +class Shape(ABC): + @abstractmethod + def calculate_area(self): + pass + +class Circle(Shape): + def __init__(self, radius): + self.radius = radius + + def calculate_area(self): + return 3.14159 * self.radius ** 2 + +class Rectangle(Shape): + def __init__(self, width, height): + self.width = width + self.height = height + + def calculate_area(self): + return self.width * self.height + +# Usage: +def process_shape(shape: Shape): + return shape.calculate_area() +``` + +**How to detect**: Search for `isinstance()`, `typeof`, or type switch patterns. + +### 6. Duplicate Code Blocks + +**Description**: Same or similar code repeated in multiple places. + +**Why it indicates test-after**: +- TDD's refactor phase explicitly targets duplication +- Each cycle includes time to eliminate redundancy +- Test-after often skips refactoring altogether + +**Example (Bad)**: +```python +def calculate_discount_price_for_books(price, quantity): + if quantity >= 10: + discount = 0.2 + elif quantity >= 5: + discount = 0.1 + else: + discount = 0 + return price * (1 - discount) + +def calculate_discount_price_for_electronics(price, quantity): + if quantity >= 10: + discount = 0.15 + elif quantity >= 5: + discount = 0.08 + else: + discount = 0 + return price * (1 - discount) +``` + +**TDD Alternative**: +```python +def calculate_discount_price(price, quantity, discount_tiers): + discount = _get_discount_for_quantity(quantity, discount_tiers) + return price * (1 - discount) + +def _get_discount_for_quantity(quantity, tiers): + for min_qty, discount in sorted(tiers.items(), reverse=True): + if quantity >= min_qty: + return discount + return 0 + +# Usage: +BOOK_DISCOUNTS = {10: 0.2, 5: 0.1} +ELECTRONICS_DISCOUNTS = {10: 0.15, 5: 0.08} + +book_price = calculate_discount_price(29.99, 12, BOOK_DISCOUNTS) +``` + +**How to detect**: Use code duplication analysis tools (>6 lines duplicated). + +### 7. Primitive Obsession + +**Description**: Using primitive types instead of small objects to represent concepts. + +**Why it indicates test-after**: +- TDD encourages creating types that make tests clearer +- Value objects emerge naturally when expressing test intent +- Primitives make tests verbose and unclear + +**Example (Bad)**: +```python +def create_appointment(patient_id, doctor_id, date_str, time_str, duration_mins): + # Working with primitives throughout + date = datetime.strptime(date_str, "%Y-%m-%d") + time = datetime.strptime(time_str, "%H:%M") + # ... complex validation and manipulation +``` + +**TDD Alternative**: +```python +@dataclass +class AppointmentTime: + date: datetime.date + time: datetime.time + duration: timedelta + + def __post_init__(self): + if self.duration <= timedelta(0): + raise ValueError("Duration must be positive") + + def end_time(self): + start = datetime.combine(self.date, self.time) + return start + self.duration + +def create_appointment(patient_id, doctor_id, appointment_time: AppointmentTime): + # Working with rich domain objects + pass +``` + +**How to detect**: Look for functions with many primitive parameters (4+). + +### 8. Comments Explaining What Code Does + +**Description**: Comments that explain the mechanics of the code rather than the "why". + +**Why it indicates test-after**: +- TDD produces self-documenting code through clear naming +- Tests serve as documentation for behavior +- Need for "what" comments suggests unclear code + +**Example (Bad)**: +```python +def process(data): + # Loop through each item in data + for item in data: + # Check if item value is greater than 100 + if item.value > 100: + # Multiply value by 1.5 + item.value = item.value * 1.5 + # Check if item is active + if item.is_active: + # Add item to results list + results.append(item) +``` + +**TDD Alternative**: +```python +def process_high_value_active_items(items): + return [apply_premium_pricing(item) + for item in items + if is_premium_eligible(item)] + +def is_premium_eligible(item): + return item.value > 100 and item.is_active + +def apply_premium_pricing(item): + item.value *= PREMIUM_MULTIPLIER + return item +``` + +**How to detect**: Look for comments explaining mechanics; good comments explain "why". + +## Low-Severity Code Smells + +### 9. Magic Numbers + +**Description**: Unexplained numeric literals scattered throughout code. + +**Example (Bad)**: +```python +def calculate_shipping(weight): + if weight < 5: + return 10 + elif weight < 20: + return 25 + else: + return 50 +``` + +**TDD Alternative**: +```python +LIGHT_PACKAGE_THRESHOLD = 5 +MEDIUM_PACKAGE_THRESHOLD = 20 +LIGHT_PACKAGE_RATE = 10 +MEDIUM_PACKAGE_RATE = 25 +HEAVY_PACKAGE_RATE = 50 + +def calculate_shipping(weight): + if weight < LIGHT_PACKAGE_THRESHOLD: + return LIGHT_PACKAGE_RATE + elif weight < MEDIUM_PACKAGE_THRESHOLD: + return MEDIUM_PACKAGE_RATE + else: + return HEAVY_PACKAGE_RATE +``` + +### 10. Long Parameter Lists + +**Description**: Methods accepting many parameters (4+). + +**Example (Bad)**: +```python +def create_user(first_name, last_name, email, phone, address, city, state, zip, country): + pass +``` + +**TDD Alternative**: +```python +@dataclass +class UserProfile: + first_name: str + last_name: str + email: str + phone: str + +@dataclass +class Address: + street: str + city: str + state: str + zip: str + country: str + +def create_user(profile: UserProfile, address: Address): + pass +``` + +## Detection Strategy + +### Automated Checks + +Run these checks regularly to identify test-after patterns: + +1. **Cyclomatic Complexity**: Flag methods with complexity > 10 +2. **Method Length**: Flag methods > 20 lines +3. **Class Size**: Flag classes with > 10 methods +4. **Nesting Depth**: Flag code with > 3 levels of nesting +5. **Duplication**: Flag blocks of > 6 duplicated lines +6. **Parameter Count**: Flag methods with > 4 parameters + +### Manual Review + +Look for these patterns during code review: + +1. Large commits with code and tests together +2. Tests that test implementation rather than behavior +3. Absence of refactoring commits +4. Complex code without corresponding complex tests +5. Tests that mock internal methods + +## Refactoring from Test-After to TDD + +If you inherit test-after code: + +1. **Add characterization tests**: Cover existing behavior +2. **Identify smells**: Use automated and manual detection +3. **Extract methods**: Break down large methods +4. **Introduce types**: Replace primitives with value objects +5. **Apply patterns**: Use polymorphism, strategy, etc. +6. **Write tests first for new features**: Start TDD from now + +Remember: The goal isn't perfect code, but continuously improving code quality through TDD discipline. diff --git a/skills/tdd-methodology-expert/references/tdd-principles.md b/skills/tdd-methodology-expert/references/tdd-principles.md new file mode 100644 index 0000000..97976dd --- /dev/null +++ b/skills/tdd-methodology-expert/references/tdd-principles.md @@ -0,0 +1,391 @@ +# Test-Driven Development (TDD) Principles + +This document provides comprehensive guidance on TDD methodology, the Red-Green-Refactor cycle, and how to apply TDD effectively across different programming contexts. + +## Core Philosophy + +Test-Driven Development is a software development methodology where tests are written before the production code. The fundamental principle is: **Write failing tests first, then write code to make them pass.** + +### Why TDD? + +1. **Design Pressure**: Writing tests first forces you to think about the API design before implementation +2. **Regression Safety**: Every feature is protected by tests from the moment it's created +3. **Living Documentation**: Tests serve as executable examples of how the code should be used +4. **Confidence to Refactor**: Comprehensive test coverage enables safe code improvements +5. **Better Code Structure**: TDD naturally produces more modular, testable, and maintainable code + +## The Red-Green-Refactor Cycle + +TDD follows a strict three-phase cycle that must be repeated for every small piece of functionality: + +### 🔴 Red Phase: Write a Failing Test + +**Purpose**: Define the desired behavior before implementation exists. + +**Process**: +1. Write a test that expresses the desired behavior +2. Run the test and verify it fails (it must fail for the right reason) +3. The failure confirms the test is actually testing something + +**Key Principles**: +- Write the simplest test that expresses one behavior +- Test should be readable and clearly express intent +- Focus on behavior, not implementation +- Use descriptive test names (e.g., `test_should_return_empty_list_when_no_items_match`) + +**Example (Python)**: +```python +# Red Phase: Test written first, will fail +def test_should_calculate_total_price_with_tax(): + cart = ShoppingCart() + cart.add_item(Item("Book", price=10.00)) + + total = cart.calculate_total(tax_rate=0.1) + + assert total == 11.00 # This will fail - method doesn't exist yet +``` + +**Red Phase Checklist**: +- [ ] Test clearly expresses desired behavior? +- [ ] Test name is descriptive and behavior-focused? +- [ ] Test actually fails when run? +- [ ] Failure message is clear about what's missing? + +### 🟢 Green Phase: Make the Test Pass + +**Purpose**: Write the minimal code necessary to make the failing test pass. + +**Process**: +1. Write just enough production code to make the test pass +2. Take shortcuts if needed (you'll refactor later) +3. Run the test and verify it passes +4. Don't add extra features or abstractions yet + +**Key Principles**: +- Write the simplest implementation that makes the test pass +- Don't worry about code quality yet (that's the refactor phase) +- Resist the urge to add features not tested +- It's okay to hard-code values initially (refactor will generalize) + +**Example (Python)**: +```python +# Green Phase: Minimal implementation to pass the test +class ShoppingCart: + def __init__(self): + self.items = [] + + def add_item(self, item): + self.items.append(item) + + def calculate_total(self, tax_rate): + # Minimal implementation - just make it work + subtotal = sum(item.price for item in self.items) + return subtotal * (1 + tax_rate) + +class Item: + def __init__(self, name, price): + self.name = name + self.price = price +``` + +**Green Phase Checklist**: +- [ ] Test now passes? +- [ ] Only added code necessary to pass the test? +- [ ] No extra features or premature optimization? +- [ ] All previously passing tests still pass? + +### 🔵 Refactor Phase: Improve the Code + +**Purpose**: Improve code quality while maintaining passing tests. + +**Process**: +1. Look for duplication, poor names, or structural issues +2. Refactor the code to improve quality +3. Run tests after each small change to ensure they still pass +4. Continue until code is clean and maintainable + +**Key Principles**: +- Never refactor with failing tests +- Make small, incremental changes +- Run tests frequently during refactoring +- Improve both test code and production code +- Apply design patterns and best practices + +**Example (Python)**: +```python +# Refactor Phase: Improve structure and readability +class ShoppingCart: + def __init__(self): + self._items = [] + + def add_item(self, item): + self._items.append(item) + + def calculate_total(self, tax_rate=0.0): + subtotal = self._calculate_subtotal() + tax_amount = subtotal * tax_rate + return subtotal + tax_amount + + def _calculate_subtotal(self): + return sum(item.price for item in self._items) + + +class Item: + def __init__(self, name, price): + if price < 0: + raise ValueError("Price cannot be negative") + self.name = name + self.price = price +``` + +**Refactor Phase Checklist**: +- [ ] Code is DRY (Don't Repeat Yourself)? +- [ ] Names are clear and expressive? +- [ ] Functions/methods are focused and small? +- [ ] Code follows SOLID principles? +- [ ] All tests still pass? +- [ ] Test code is also clean and maintainable? + +## TDD Rhythm and Cadence + +### The Cycle Duration + +A complete Red-Green-Refactor cycle should be **very short** - typically 2-10 minutes: +- Red: 1-2 minutes to write a small test +- Green: 1-5 minutes to make it pass +- Refactor: 1-3 minutes to clean up + +If cycles are taking longer, the tests are too large. Break them into smaller pieces. + +### Commit Strategy + +Commit at the end of each complete Red-Green-Refactor cycle: +- After Red: Don't commit failing tests +- After Green: Can commit if needed, but prefer to refactor first +- After Refactor: **Commit here** - you have working, clean code with tests + +### Incremental Development + +Build features incrementally, one test at a time: + +**Bad (Big Bang)**: +``` +Write comprehensive test suite → Implement entire feature → Debug for hours +``` + +**Good (Incremental)**: +``` +Test 1 → Implement 1 → Refactor → +Test 2 → Implement 2 → Refactor → +Test 3 → Implement 3 → Refactor → ... +``` + +## TDD Best Practices + +### 1. Test Behavior, Not Implementation + +**Bad**: +```python +def test_uses_quicksort_algorithm(): + sorter = Sorter() + # Testing internal implementation detail + assert sorter._partition_method == "quicksort" +``` + +**Good**: +```python +def test_should_return_sorted_list_in_ascending_order(): + sorter = Sorter() + unsorted = [3, 1, 4, 1, 5] + + result = sorter.sort(unsorted) + + assert result == [1, 1, 3, 4, 5] +``` + +### 2. One Assertion Per Test (Usually) + +**Bad**: +```python +def test_user_registration(): + user = register_user("john@example.com", "password123") + + assert user.email == "john@example.com" + assert user.is_active == True + assert user.created_at is not None + assert user.id is not None + # Too many concerns in one test +``` + +**Good**: +```python +def test_should_create_user_with_provided_email(): + user = register_user("john@example.com", "password123") + assert user.email == "john@example.com" + +def test_should_create_active_user_by_default(): + user = register_user("john@example.com", "password123") + assert user.is_active == True + +def test_should_assign_creation_timestamp_to_new_user(): + user = register_user("john@example.com", "password123") + assert user.created_at is not None +``` + +### 3. Arrange-Act-Assert (AAA) Pattern + +Structure every test with three clear sections: + +```python +def test_should_withdraw_amount_from_account_balance(): + # Arrange: Set up test data and preconditions + account = Account(initial_balance=100) + + # Act: Execute the behavior being tested + account.withdraw(30) + + # Assert: Verify the expected outcome + assert account.balance == 70 +``` + +### 4. Test Names Should Be Descriptive + +**Bad**: `test_user()`, `test_1()`, `test_validation()` + +**Good**: +- `test_should_reject_user_registration_with_invalid_email()` +- `test_should_return_empty_list_when_database_has_no_records()` +- `test_should_throw_exception_when_withdrawal_exceeds_balance()` + +### 5. Keep Tests Fast + +- Unit tests should run in milliseconds +- Avoid file I/O, network calls, and databases in unit tests +- Use mocks/stubs for external dependencies +- Slow tests discourage running them frequently + +### 6. Keep Tests Independent + +Each test should be able to run in isolation: +- Don't rely on test execution order +- Don't share state between tests +- Use setup/teardown to create clean state for each test + +## Common TDD Mistakes + +### Mistake 1: Writing Tests After Code + +**Problem**: Writing tests after implementation defeats the purpose of TDD. + +**Why it matters**: You lose the design benefits of TDD and tests become implementation-focused rather than behavior-focused. + +**Solution**: Discipline. Always write the test first, even if it feels slower initially. + +### Mistake 2: Tests That Are Too Large + +**Problem**: Writing comprehensive tests that cover too much functionality. + +**Why it matters**: Large tests lead to large implementations, losing the incremental nature of TDD. + +**Solution**: Break down tests into smallest possible behavioral units. If a test takes more than 5 minutes to implement, it's too big. + +### Mistake 3: Skipping the Refactor Phase + +**Problem**: Moving to the next test immediately after green without refactoring. + +**Why it matters**: Technical debt accumulates quickly, leading to unmaintainable code. + +**Solution**: Always spend time in the refactor phase. Code quality is not optional. + +### Mistake 4: Testing Implementation Details + +**Problem**: Tests that check how code works internally rather than what it produces. + +**Why it matters**: Implementation-focused tests are fragile and prevent refactoring. + +**Solution**: Focus tests on public interfaces and observable behavior. + +### Mistake 5: Incomplete Red Phase + +**Problem**: Not running the test to verify it actually fails. + +**Why it matters**: You might write a test that always passes (false positive). + +**Solution**: Always verify the test fails before implementing. Watch it turn from red to green. + +## TDD in Different Contexts + +### Unit Testing (Primary TDD Focus) + +- Test individual functions/methods in isolation +- Fast execution (milliseconds) +- Mock external dependencies +- High coverage of edge cases + +### Integration Testing (TDD Can Apply) + +- Test interactions between components +- Slower execution (seconds) +- Use real dependencies where practical +- Focus on critical integration points + +### Acceptance Testing (BDD/TDD Hybrid) + +- Test complete user scenarios +- Written in behavior-focused language +- Slower execution (seconds to minutes) +- Validate business requirements + +## Measuring TDD Effectiveness + +### Good TDD Indicators + +1. **Test Count**: Tests outnumber production code files +2. **Test Coverage**: High line/branch coverage (>80%) +3. **Commit Frequency**: Small, frequent commits +4. **Code Structure**: Small functions, low complexity +5. **Refactoring Confidence**: Easy to change code without fear + +### Poor TDD Indicators + +1. **Test-After Pattern**: Tests written in bulk after features +2. **Low Coverage**: Large portions of code untested +3. **Complex Code**: Long methods, deep nesting +4. **Fragile Tests**: Tests break with minor refactoring +5. **Fear of Change**: Reluctance to modify code + +## TDD and Code Quality + +### Code Characteristics of TDD + +Well-executed TDD produces code with: + +✅ **Small Functions**: Typically 5-20 lines +✅ **Flat Structure**: Minimal nesting (1-2 levels max) +✅ **Single Responsibility**: Each function does one thing +✅ **Clear Naming**: Descriptive names that explain purpose +✅ **Low Coupling**: Components loosely connected +✅ **High Cohesion**: Related functionality grouped together +✅ **Testable Design**: Easy to test in isolation + +### Code Smells Indicating Test-After Development + +❌ **Long Methods**: Functions over 30 lines +❌ **Deep Nesting**: 3+ levels of if/else/for statements +❌ **Complex Conditionals**: Multiple AND/OR in one condition +❌ **Type Checking**: Using `isinstance()` or `typeof` +❌ **God Objects**: Classes with too many responsibilities +❌ **Tight Coupling**: Components highly dependent on each other +❌ **Poor Naming**: Generic names like `data`, `process`, `manager` + +## Summary: The TDD Mindset + +TDD is not just about testing - it's a design methodology: + +1. **Tests Drive Design**: Let test requirements shape your API +2. **Incremental Progress**: Build features one small test at a time +3. **Continuous Refinement**: Always improve code quality through refactoring +4. **Fast Feedback**: Run tests constantly to catch issues immediately +5. **Confidence**: Trust your tests to enable bold refactoring + +Remember: **Red-Green-Refactor** is not optional. Follow the cycle religiously, and your code quality will improve dramatically. diff --git a/skills/tdd-methodology-expert/references/testing-patterns.md b/skills/tdd-methodology-expert/references/testing-patterns.md new file mode 100644 index 0000000..4d46ec7 --- /dev/null +++ b/skills/tdd-methodology-expert/references/testing-patterns.md @@ -0,0 +1,637 @@ +# Testing Patterns for TDD + +This document provides language-agnostic testing patterns and best practices that support effective Test-Driven Development across different programming environments. + +## Test Structure Patterns + +### The AAA Pattern (Arrange-Act-Assert) + +The most fundamental testing pattern. Every test should follow this three-part structure: + +``` +Arrange: Set up the test data and preconditions +Act: Execute the behavior being tested +Assert: Verify the expected outcome +``` + +**Example (Python)**: +```python +def test_should_add_item_to_shopping_cart(): + # Arrange + cart = ShoppingCart() + item = Item("Book", price=29.99) + + # Act + cart.add_item(item) + + # Assert + assert len(cart.items) == 1 + assert cart.items[0] == item +``` + +**Example (JavaScript)**: +```javascript +test('should add item to shopping cart', () => { + // Arrange + const cart = new ShoppingCart(); + const item = new Item("Book", 29.99); + + // Act + cart.addItem(item); + + // Assert + expect(cart.items).toHaveLength(1); + expect(cart.items[0]).toBe(item); +}); +``` + +### Given-When-Then (BDD Style) + +An alternative to AAA, commonly used in Behavior-Driven Development: + +``` +Given: Preconditions and context +When: The action or event +Then: Expected outcomes +``` + +**Example (Ruby)**: +```ruby +describe 'Shopping Cart' do + it 'adds item to cart' do + # Given a cart and an item + cart = ShoppingCart.new + item = Item.new("Book", 29.99) + + # When adding the item + cart.add_item(item) + + # Then the cart should contain the item + expect(cart.items.length).to eq(1) + expect(cart.items[0]).to eq(item) + end +end +``` + +## Test Organization Patterns + +### Test Fixture Pattern + +Use setup/teardown to create consistent test preconditions: + +**Example (Python with pytest)**: +```python +import pytest + +@pytest.fixture +def cart(): + """Create a fresh shopping cart for each test""" + return ShoppingCart() + +@pytest.fixture +def sample_items(): + """Create sample items for testing""" + return [ + Item("Book", 29.99), + Item("Pen", 1.99), + Item("Notebook", 5.99) + ] + +def test_should_calculate_correct_total(cart, sample_items): + for item in sample_items: + cart.add_item(item) + + total = cart.calculate_total() + + assert total == 37.97 +``` + +**Example (JavaScript with Jest)**: +```javascript +describe('ShoppingCart', () => { + let cart; + + beforeEach(() => { + cart = new ShoppingCart(); + }); + + test('should calculate correct total', () => { + cart.addItem(new Item("Book", 29.99)); + cart.addItem(new Item("Pen", 1.99)); + + const total = cart.calculateTotal(); + + expect(total).toBe(31.98); + }); +}); +``` + +### Test Builder Pattern + +Create fluent APIs for complex test data setup: + +**Example (Java)**: +```java +public class OrderBuilder { + private Customer customer; + private List items = new ArrayList<>(); + private PaymentMethod paymentMethod; + + public OrderBuilder withCustomer(String name, String email) { + this.customer = new Customer(name, email); + return this; + } + + public OrderBuilder withItem(String name, double price) { + this.items.add(new Item(name, price)); + return this; + } + + public OrderBuilder withPayment(PaymentMethod method) { + this.paymentMethod = method; + return this; + } + + public Order build() { + Order order = new Order(customer); + items.forEach(order::addItem); + order.setPaymentMethod(paymentMethod); + return order; + } +} + +// Usage in tests: +@Test +public void shouldProcessOrderSuccessfully() { + Order order = new OrderBuilder() + .withCustomer("John Doe", "john@example.com") + .withItem("Book", 29.99) + .withItem("Pen", 1.99) + .withPayment(PaymentMethod.CREDIT_CARD) + .build(); + + OrderResult result = processor.process(order); + + assertEquals(OrderStatus.COMPLETED, result.getStatus()); +} +``` + +### Object Mother Pattern + +Centralized factory for creating common test objects: + +**Example (Python)**: +```python +class CustomerMother: + """Factory for creating test customers""" + + @staticmethod + def create_standard_customer(): + return Customer( + name="John Doe", + email="john@example.com", + is_active=True + ) + + @staticmethod + def create_vip_customer(): + return Customer( + name="Jane Smith", + email="jane@example.com", + is_active=True, + membership_level="VIP" + ) + + @staticmethod + def create_inactive_customer(): + return Customer( + name="Bob Wilson", + email="bob@example.com", + is_active=False + ) + +# Usage in tests: +def test_should_apply_vip_discount(): + customer = CustomerMother.create_vip_customer() + cart = ShoppingCart(customer) + + cart.add_item(Item("Book", 100)) + total = cart.calculate_total() + + assert total == 80 # 20% VIP discount +``` + +## Assertion Patterns + +### Single Assertion Principle + +**Guideline**: Each test should verify one logical concept (though may have multiple assertion statements for clarity). + +**Good**: +```python +def test_should_create_user_with_correct_attributes(): + user = create_user("john@example.com", "John Doe") + + # Multiple assertions verifying one concept: user creation + assert user.email == "john@example.com" + assert user.name == "John Doe" + assert user.is_active is True +``` + +**Better (when concepts are truly separate)**: +```python +def test_should_create_user_with_provided_email(): + user = create_user("john@example.com", "John Doe") + assert user.email == "john@example.com" + +def test_should_create_user_with_provided_name(): + user = create_user("john@example.com", "John Doe") + assert user.name == "John Doe" + +def test_should_create_active_user_by_default(): + user = create_user("john@example.com", "John Doe") + assert user.is_active is True +``` + +### Custom Assertion Methods + +Create domain-specific assertions for clarity: + +**Example (JavaScript)**: +```javascript +function assertValidOrder(order) { + expect(order.customer).toBeDefined(); + expect(order.items).not.toHaveLength(0); + expect(order.total).toBeGreaterThan(0); + expect(order.status).toBe('pending'); +} + +test('should create valid order from cart', () => { + const cart = createSampleCart(); + + const order = cart.checkout(); + + assertValidOrder(order); +}); +``` + +## Test Doubles (Mocking) Patterns + +### Stub Pattern + +Replace dependencies with simplified implementations: + +**Example (Python)**: +```python +class StubEmailService: + """Stub that tracks calls without sending real emails""" + def __init__(self): + self.sent_emails = [] + + def send(self, to, subject, body): + self.sent_emails.append({ + 'to': to, + 'subject': subject, + 'body': body + }) + +def test_should_send_welcome_email_on_registration(): + email_service = StubEmailService() + user_service = UserService(email_service) + + user = user_service.register("john@example.com", "password") + + assert len(email_service.sent_emails) == 1 + assert email_service.sent_emails[0]['to'] == "john@example.com" + assert "Welcome" in email_service.sent_emails[0]['subject'] +``` + +### Mock Pattern + +Verify interactions with dependencies: + +**Example (JavaScript with Jest)**: +```javascript +test('should call payment gateway with correct amount', () => { + const mockGateway = { + charge: jest.fn().mockResolvedValue({ success: true }) + }; + const processor = new PaymentProcessor(mockGateway); + + processor.processPayment(customer, 100.00); + + expect(mockGateway.charge).toHaveBeenCalledWith( + customer.paymentToken, + 100.00 + ); +}); +``` + +### Fake Pattern + +Provide working implementations with shortcuts: + +**Example (Python)**: +```python +class FakeUserRepository: + """In-memory repository for testing""" + def __init__(self): + self.users = {} + self.next_id = 1 + + def save(self, user): + if not user.id: + user.id = self.next_id + self.next_id += 1 + self.users[user.id] = user + return user + + def find_by_id(self, user_id): + return self.users.get(user_id) + +def test_should_persist_user_with_generated_id(): + repo = FakeUserRepository() + user = User(name="John") + + saved_user = repo.save(user) + + assert saved_user.id is not None + assert repo.find_by_id(saved_user.id) == saved_user +``` + +## Parameterized Testing Pattern + +Test multiple cases with the same structure: + +**Example (Python with pytest)**: +```python +import pytest + +@pytest.mark.parametrize("input,expected", [ + (0, 0), + (1, 1), + (2, 2), + (3, 6), + (4, 24), + (5, 120), +]) +def test_factorial_calculation(input, expected): + result = factorial(input) + assert result == expected +``` + +**Example (JavaScript with Jest)**: +```javascript +test.each([ + [0, 0], + [1, 1], + [2, 2], + [3, 6], + [4, 24], + [5, 120], +])('factorial(%i) should equal %i', (input, expected) => { + const result = factorial(input); + expect(result).toBe(expected); +}); +``` + +## Exception Testing Pattern + +Test error conditions explicitly: + +**Example (Python)**: +```python +def test_should_raise_error_when_withdrawing_too_much(): + account = Account(balance=100) + + with pytest.raises(InsufficientFundsError) as exc_info: + account.withdraw(150) + + assert "Insufficient funds" in str(exc_info.value) + assert exc_info.value.available == 100 + assert exc_info.value.requested == 150 +``` + +**Example (JavaScript)**: +```javascript +test('should throw error when withdrawing too much', () => { + const account = new Account(100); + + expect(() => { + account.withdraw(150); + }).toThrow(InsufficientFundsError); + + expect(() => { + account.withdraw(150); + }).toThrow('Insufficient funds'); +}); +``` + +## Property-Based Testing Pattern + +Test properties that should always hold: + +**Example (Python with hypothesis)**: +```python +from hypothesis import given +import hypothesis.strategies as st + +@given(st.lists(st.integers())) +def test_reversing_twice_gives_original(lst): + result = reverse(reverse(lst)) + assert result == lst + +@given(st.integers(min_value=0)) +def test_factorial_is_positive(n): + result = factorial(n) + assert result > 0 +``` + +## State-Based vs. Interaction-Based Testing + +### State-Based Testing (Preferred) + +Verify final state rather than how it was achieved: + +**Example**: +```python +def test_should_remove_item_from_cart(): + cart = ShoppingCart() + item = Item("Book", 29.99) + cart.add_item(item) + + cart.remove_item(item) + + assert len(cart.items) == 0 # Verify state +``` + +### Interaction-Based Testing + +Verify interactions when state is not observable: + +**Example**: +```python +def test_should_log_failed_login_attempt(): + logger = Mock() + auth = AuthService(logger) + + auth.login("user", "wrong_password") + + logger.warning.assert_called_once_with( + "Failed login attempt for user: user" + ) +``` + +## Test Naming Patterns + +### Should-Style Naming + +``` +test_should__when_ +``` + +**Examples**: +- `test_should_return_empty_list_when_no_matches_found` +- `test_should_throw_exception_when_amount_is_negative` +- `test_should_apply_discount_when_quantity_exceeds_ten` + +### Behavior-Style Naming + +``` +test___ +``` + +**Examples**: +- `test_cart_with_multiple_items_calculates_correct_total` +- `test_user_registration_with_invalid_email_fails` +- `test_payment_processing_with_insufficient_funds_raises_error` + +## Test Data Patterns + +### Obvious Data + +Use self-documenting test data: + +**Bad**: +```python +def test_calculation(): + result = calculate(10, 5, 3) + assert result == 8 +``` + +**Good**: +```python +def test_should_calculate_average_correctly(): + value1 = 10 + value2 = 20 + value3 = 30 + expected_average = 20 + + result = calculate_average([value1, value2, value3]) + + assert result == expected_average +``` + +### Boundary Value Testing + +Test edges of valid ranges: + +```python +def test_age_validation(): + validator = AgeValidator(min_age=18, max_age=100) + + # Below minimum + assert not validator.is_valid(17) + + # At minimum boundary + assert validator.is_valid(18) + + # Normal value + assert validator.is_valid(50) + + # At maximum boundary + assert validator.is_valid(100) + + # Above maximum + assert not validator.is_valid(101) +``` + +## TDD-Specific Patterns + +### Triangulation + +Build generality through multiple test cases: + +**Step 1 - Specific case**: +```python +def test_should_add_two_numbers(): + assert add(2, 3) == 5 + +# Implementation: +def add(a, b): + return 5 # Hardcoded - simplest thing that works +``` + +**Step 2 - Add another case to force generalization**: +```python +def test_should_add_two_numbers(): + assert add(2, 3) == 5 + +def test_should_add_different_numbers(): + assert add(4, 7) == 11 + +# Implementation: +def add(a, b): + return a + b # Now forced to generalize +``` + +### Fake It Till You Make It + +Start with simple/hardcoded implementations: + +```python +# Test +def test_should_return_greeting(): + assert greet("World") == "Hello, World!" + +# First implementation (fake it) +def greet(name): + return "Hello, World!" + +# Add another test to force real implementation +def test_should_return_greeting_with_different_name(): + assert greet("Alice") == "Hello, Alice!" + +# Real implementation +def greet(name): + return f"Hello, {name}!" +``` + +### Obvious Implementation + +When the implementation is obvious, just write it: + +```python +# Test +def test_should_calculate_rectangle_area(): + assert calculate_area(width=5, height=3) == 15 + +# Implementation (obvious, no need to fake) +def calculate_area(width, height): + return width * height +``` + +## Summary: Testing Pattern Selection + +- **Use AAA** for clarity in most tests +- **Use fixtures** to eliminate setup duplication +- **Use builders** for complex object creation +- **Use test doubles** when dependencies are expensive or unpredictable +- **Prefer state-based** testing over interaction-based +- **Use parameterized tests** for multiple similar cases +- **Test boundaries** explicitly +- **Name tests** to describe behavior +- **Keep tests independent** of each other +- **Make tests readable** - they're documentation + +Remember: Tests are first-class citizens in TDD. Treat them with the same care as production code. diff --git a/skills/tdd-methodology-expert/scripts/check_tdd_compliance.py b/skills/tdd-methodology-expert/scripts/check_tdd_compliance.py new file mode 100644 index 0000000..5f5e6d4 --- /dev/null +++ b/skills/tdd-methodology-expert/scripts/check_tdd_compliance.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +""" +TDD Compliance Checker + +Analyzes code to detect if Test-Driven Development was followed. +Identifies code smells and patterns that indicate tests-after-code. +""" + +import os +import re +import sys +import json +from pathlib import Path +from typing import Dict, List, Tuple + + +class TDDComplianceChecker: + """Checks code for TDD compliance indicators.""" + + # Code smell patterns that suggest tests-after-code + CODE_SMELLS = { + 'nested_conditionals': r'if\s+.*:\s*\n\s+if\s+.*:|if\s+.*:\s*\n\s+elif\s+', + 'long_methods': None, # Checked by line count + 'complex_conditions': r'if\s+.*\s+(and|or)\s+.*\s+(and|or)\s+', + 'multiple_responsibilities': None, # Checked by method analysis + 'missing_abstractions': r'if\s+isinstance\(', + 'god_class': None, # Checked by class analysis + } + + def __init__(self, path: str): + self.path = Path(path) + self.issues = [] + self.metrics = { + 'files_analyzed': 0, + 'test_files_found': 0, + 'code_smells': 0, + 'tdd_score': 0.0 + } + + def analyze(self) -> Dict: + """Run full TDD compliance analysis.""" + if self.path.is_file(): + self._analyze_file(self.path) + else: + self._analyze_directory(self.path) + + self._calculate_tdd_score() + return { + 'issues': self.issues, + 'metrics': self.metrics, + 'compliance': self._get_compliance_level() + } + + def _analyze_directory(self, directory: Path): + """Recursively analyze all source files in directory.""" + # Common source file extensions + extensions = {'.py', '.js', '.ts', '.java', '.go', '.rb', '.php', '.c', '.cpp', '.cs'} + + for file_path in directory.rglob('*'): + if file_path.suffix in extensions and file_path.is_file(): + # Skip test files in analysis (we'll check they exist separately) + if not self._is_test_file(file_path): + self._analyze_file(file_path) + + def _analyze_file(self, file_path: Path): + """Analyze a single source file for TDD compliance.""" + self.metrics['files_analyzed'] += 1 + + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + lines = content.split('\n') + + # Check for code smells + self._check_nested_conditionals(file_path, content) + self._check_long_methods(file_path, lines) + self._check_complex_conditions(file_path, content) + self._check_missing_abstractions(file_path, content) + + # Check if corresponding test file exists + self._check_test_coverage(file_path) + + except Exception as e: + self.issues.append({ + 'file': str(file_path), + 'type': 'error', + 'message': f'Failed to analyze file: {str(e)}' + }) + + def _check_nested_conditionals(self, file_path: Path, content: str): + """Detect deeply nested conditional statements.""" + pattern = self.CODE_SMELLS['nested_conditionals'] + matches = re.finditer(pattern, content) + + for match in matches: + line_num = content[:match.start()].count('\n') + 1 + self.issues.append({ + 'file': str(file_path), + 'line': line_num, + 'type': 'code_smell', + 'severity': 'high', + 'smell': 'nested_conditionals', + 'message': 'Nested conditional statements detected. TDD typically produces flatter, more testable code structures.' + }) + self.metrics['code_smells'] += 1 + + def _check_long_methods(self, file_path: Path, lines: List[str]): + """Detect methods/functions that are too long.""" + # Simple heuristic: methods longer than 20 lines + in_method = False + method_start = 0 + method_name = '' + indent_level = 0 + + for i, line in enumerate(lines): + stripped = line.lstrip() + + # Detect method/function definitions (language-agnostic patterns) + if any(keyword in stripped for keyword in ['def ', 'function ', 'func ', 'public ', 'private ', 'protected ']): + if '{' in stripped or ':' in stripped: + in_method = True + method_start = i + 1 + method_name = stripped.split('(')[0].split()[-1] + indent_level = len(line) - len(stripped) + + # Check if method ended + elif in_method: + current_indent = len(line) - len(line.lstrip()) + if stripped and current_indent <= indent_level and stripped not in ['}', 'end']: + method_length = i - method_start + if method_length > 20: + self.issues.append({ + 'file': str(file_path), + 'line': method_start, + 'type': 'code_smell', + 'severity': 'medium', + 'smell': 'long_method', + 'message': f'Method "{method_name}" is {method_length} lines long. TDD encourages smaller, focused methods.' + }) + self.metrics['code_smells'] += 1 + in_method = False + + def _check_complex_conditions(self, file_path: Path, content: str): + """Detect overly complex conditional expressions.""" + pattern = self.CODE_SMELLS['complex_conditions'] + matches = re.finditer(pattern, content) + + for match in matches: + line_num = content[:match.start()].count('\n') + 1 + self.issues.append({ + 'file': str(file_path), + 'line': line_num, + 'type': 'code_smell', + 'severity': 'medium', + 'smell': 'complex_conditions', + 'message': 'Complex boolean conditions detected. TDD promotes simpler, more testable conditions.' + }) + self.metrics['code_smells'] += 1 + + def _check_missing_abstractions(self, file_path: Path, content: str): + """Detect type checking that suggests missing abstractions.""" + pattern = self.CODE_SMELLS['missing_abstractions'] + matches = re.finditer(pattern, content) + + for match in matches: + line_num = content[:match.start()].count('\n') + 1 + self.issues.append({ + 'file': str(file_path), + 'line': line_num, + 'type': 'code_smell', + 'severity': 'medium', + 'smell': 'missing_abstractions', + 'message': 'Type checking detected. TDD encourages polymorphism over type checking.' + }) + self.metrics['code_smells'] += 1 + + def _check_test_coverage(self, file_path: Path): + """Check if a corresponding test file exists.""" + test_file = self._find_test_file(file_path) + + if test_file and test_file.exists(): + self.metrics['test_files_found'] += 1 + else: + self.issues.append({ + 'file': str(file_path), + 'type': 'missing_test', + 'severity': 'critical', + 'message': f'No corresponding test file found. Expected: {test_file}' + }) + + def _find_test_file(self, source_file: Path) -> Path: + """Find the expected test file location for a source file.""" + # Common test file patterns + test_patterns = [ + lambda p: p.parent / f'test_{p.name}', + lambda p: p.parent / f'{p.stem}_test{p.suffix}', + lambda p: p.parent / 'tests' / f'test_{p.name}', + lambda p: p.parent.parent / 'tests' / p.parent.name / f'test_{p.name}', + lambda p: p.parent.parent / 'test' / p.parent.name / f'test_{p.name}', + ] + + for pattern in test_patterns: + test_file = pattern(source_file) + if test_file.exists(): + return test_file + + # Return the most common pattern as expected location + return source_file.parent / f'test_{source_file.name}' + + def _is_test_file(self, file_path: Path) -> bool: + """Check if a file is a test file.""" + name = file_path.name.lower() + return any([ + name.startswith('test_'), + name.endswith('_test.py'), + name.endswith('_test.js'), + name.endswith('.test.js'), + name.endswith('.spec.js'), + 'test' in file_path.parts, + 'tests' in file_path.parts, + ]) + + def _calculate_tdd_score(self): + """Calculate an overall TDD compliance score (0-100).""" + if self.metrics['files_analyzed'] == 0: + self.metrics['tdd_score'] = 0.0 + return + + # Factors that contribute to score + test_coverage_ratio = self.metrics['test_files_found'] / self.metrics['files_analyzed'] + smell_penalty = min(self.metrics['code_smells'] * 5, 50) # Max 50 point penalty + + # Score calculation + score = (test_coverage_ratio * 100) - smell_penalty + self.metrics['tdd_score'] = max(0.0, min(100.0, score)) + + def _get_compliance_level(self) -> str: + """Get human-readable compliance level.""" + score = self.metrics['tdd_score'] + + if score >= 90: + return 'excellent' + elif score >= 75: + return 'good' + elif score >= 50: + return 'fair' + elif score >= 25: + return 'poor' + else: + return 'critical' + + +def main(): + """Main entry point for the TDD compliance checker.""" + if len(sys.argv) < 2: + print("Usage: check_tdd_compliance.py ") + print(" path: File or directory to analyze") + sys.exit(1) + + path = sys.argv[1] + + if not os.path.exists(path): + print(f"Error: Path '{path}' does not exist") + sys.exit(1) + + checker = TDDComplianceChecker(path) + results = checker.analyze() + + # Output results as JSON + print(json.dumps(results, indent=2)) + + # Exit with appropriate code + if results['compliance'] in ['critical', 'poor']: + sys.exit(1) + else: + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/skills/tdd-methodology-expert/scripts/setup_hooks.sh b/skills/tdd-methodology-expert/scripts/setup_hooks.sh new file mode 100644 index 0000000..381afec --- /dev/null +++ b/skills/tdd-methodology-expert/scripts/setup_hooks.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# +# Setup TDD Hooks +# +# Installs git hooks and Claude Code hooks for TDD enforcement. +# This script should be run once per project to enable TDD reinforcement. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(dirname "$SCRIPT_DIR")" +PROJECT_ROOT="${1:-.}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "🔧 Setting up TDD hooks for project: $PROJECT_ROOT" + +# Check if git repository +if [ ! -d "$PROJECT_ROOT/.git" ]; then + echo -e "${YELLOW}Warning: Not a git repository. Skipping git hooks.${NC}" + GIT_HOOKS=false +else + GIT_HOOKS=true +fi + +# Check if Claude Code project +if [ ! -d "$PROJECT_ROOT/.claude" ]; then + echo -e "${YELLOW}Warning: Not a Claude Code project. Creating .claude directory.${NC}" + mkdir -p "$PROJECT_ROOT/.claude" +fi + +# Install git pre-commit hook +if [ "$GIT_HOOKS" = true ]; then + echo "📝 Installing git pre-commit hook..." + HOOKS_DIR="$PROJECT_ROOT/.git/hooks" + mkdir -p "$HOOKS_DIR" + + # Copy pre-commit hook template + cp "$SKILL_DIR/assets/hook-templates/pre-commit.sh" "$HOOKS_DIR/pre-commit" + chmod +x "$HOOKS_DIR/pre-commit" + + echo -e "${GREEN}✅ Git pre-commit hook installed${NC}" +fi + +# Install Claude Code user-prompt-submit hook +echo "📝 Installing Claude Code user-prompt-submit hook..." +CLAUDE_HOOKS_DIR="$PROJECT_ROOT/.claude/hooks" +mkdir -p "$CLAUDE_HOOKS_DIR" + +# Copy user-prompt-submit hook template +cp "$SKILL_DIR/assets/hook-templates/user-prompt-submit.sh" "$CLAUDE_HOOKS_DIR/user-prompt-submit" +chmod +x "$CLAUDE_HOOKS_DIR/user-prompt-submit" + +echo -e "${GREEN}✅ Claude Code user-prompt-submit hook installed${NC}" + +# Create or update CLAUDE.md to mention TDD +CLAUDE_MD="$PROJECT_ROOT/.claude/CLAUDE.md" +if [ ! -f "$CLAUDE_MD" ]; then + echo "📝 Creating CLAUDE.md with TDD requirement..." + cat > "$CLAUDE_MD" << 'EOF' +# Project Guidelines + +## Development Methodology + +**This project uses Test-Driven Development (TDD).** + +All code must be developed following the Red-Green-Refactor cycle: +1. 🔴 Red: Write a failing test first +2. 🟢 Green: Write minimal code to make the test pass +3. 🔵 Refactor: Improve code while keeping tests green + +The `tdd-methodology-expert` skill is automatically loaded for this project. +EOF + echo -e "${GREEN}✅ CLAUDE.md created with TDD requirement${NC}" +else + # Check if TDD is already mentioned + if ! grep -q "TDD\|Test-Driven Development" "$CLAUDE_MD"; then + echo "📝 Updating CLAUDE.md with TDD requirement..." + echo "" >> "$CLAUDE_MD" + echo "## Development Methodology" >> "$CLAUDE_MD" + echo "" >> "$CLAUDE_MD" + echo "**This project uses Test-Driven Development (TDD).**" >> "$CLAUDE_MD" + echo "" >> "$CLAUDE_MD" + echo "All code must be developed following the Red-Green-Refactor cycle." >> "$CLAUDE_MD" + echo "The \`tdd-methodology-expert\` skill is automatically loaded for this project." >> "$CLAUDE_MD" + echo -e "${GREEN}✅ CLAUDE.md updated with TDD requirement${NC}" + else + echo -e "${GREEN}✅ CLAUDE.md already mentions TDD${NC}" + fi +fi + +# Make scripts executable +chmod +x "$SKILL_DIR/scripts/"*.py + +echo "" +echo -e "${GREEN}✅ TDD hooks setup complete!${NC}" +echo "" +echo "The following hooks have been installed:" +if [ "$GIT_HOOKS" = true ]; then + echo " • Git pre-commit hook: Validates TDD compliance before commits" +fi +echo " • Claude Code user-prompt-submit hook: Reinforces TDD in every interaction" +echo "" +echo "To verify installation, run:" +if [ "$GIT_HOOKS" = true ]; then + echo " git hook run pre-commit" +fi +echo " cat $PROJECT_ROOT/.claude/hooks/user-prompt-submit" diff --git a/skills/tdd-methodology-expert/scripts/validate_tests.py b/skills/tdd-methodology-expert/scripts/validate_tests.py new file mode 100644 index 0000000..4a7f47d --- /dev/null +++ b/skills/tdd-methodology-expert/scripts/validate_tests.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +""" +Test Validator + +Validates that tests exist, are properly structured, and follow TDD principles. +""" + +import os +import re +import sys +import json +from pathlib import Path +from typing import Dict, List, Set + + +class TestValidator: + """Validates test files for TDD compliance.""" + + def __init__(self, path: str): + self.path = Path(path) + self.results = { + 'valid': True, + 'test_files': [], + 'issues': [], + 'stats': { + 'total_tests': 0, + 'test_files_found': 0, + 'well_structured': 0 + } + } + + def validate(self) -> Dict: + """Run full test validation.""" + test_files = self._find_test_files() + + if not test_files: + self.results['valid'] = False + self.results['issues'].append({ + 'type': 'no_tests', + 'severity': 'critical', + 'message': 'No test files found. TDD requires writing tests first.' + }) + return self.results + + for test_file in test_files: + self._validate_test_file(test_file) + + return self.results + + def _find_test_files(self) -> List[Path]: + """Find all test files in the path.""" + test_files = [] + + if self.path.is_file(): + if self._is_test_file(self.path): + test_files.append(self.path) + else: + for file_path in self.path.rglob('*'): + if file_path.is_file() and self._is_test_file(file_path): + test_files.append(file_path) + + return test_files + + def _is_test_file(self, file_path: Path) -> bool: + """Check if a file is a test file.""" + name = file_path.name.lower() + return any([ + name.startswith('test_'), + name.endswith('_test.py'), + name.endswith('_test.js'), + name.endswith('.test.js'), + name.endswith('.test.ts'), + name.endswith('.spec.js'), + name.endswith('.spec.ts'), + name.endswith('Test.java'), + name.endswith('_test.go'), + 'test' in file_path.parts, + 'tests' in file_path.parts, + ]) + + def _validate_test_file(self, test_file: Path): + """Validate a single test file.""" + self.results['stats']['test_files_found'] += 1 + file_result = { + 'file': str(test_file), + 'tests_found': 0, + 'issues': [] + } + + try: + with open(test_file, 'r', encoding='utf-8') as f: + content = f.read() + + # Count test cases + test_count = self._count_tests(content, test_file.suffix) + file_result['tests_found'] = test_count + self.results['stats']['total_tests'] += test_count + + if test_count == 0: + file_result['issues'].append({ + 'type': 'empty_test_file', + 'severity': 'high', + 'message': 'Test file contains no test cases' + }) + self.results['valid'] = False + + # Check test structure + structure_issues = self._check_test_structure(content, test_file) + file_result['issues'].extend(structure_issues) + + if not structure_issues: + self.results['stats']['well_structured'] += 1 + + # Check for TDD patterns + tdd_issues = self._check_tdd_patterns(content, test_file) + file_result['issues'].extend(tdd_issues) + + except Exception as e: + file_result['issues'].append({ + 'type': 'error', + 'severity': 'high', + 'message': f'Failed to validate test file: {str(e)}' + }) + self.results['valid'] = False + + self.results['test_files'].append(file_result) + + # Aggregate issues + for issue in file_result['issues']: + if issue['severity'] in ['critical', 'high']: + self.results['valid'] = False + self.results['issues'].append({ + 'file': str(test_file), + **issue + }) + + def _count_tests(self, content: str, extension: str) -> int: + """Count the number of test cases in the file.""" + count = 0 + + # Language-specific test detection patterns + patterns = { + '.py': [r'def test_\w+', r'@pytest\.mark\.'], + '.js': [r'(test|it)\s*\(', r'describe\s*\('], + '.ts': [r'(test|it)\s*\(', r'describe\s*\('], + '.java': [r'@Test', r'public\s+void\s+test\w+'], + '.go': [r'func\s+Test\w+'], + '.rb': [r'(it|test)\s+["\']', r'describe\s+["\']'], + '.php': [r'public\s+function\s+test\w+', r'@test'], + } + + if extension in patterns: + for pattern in patterns[extension]: + count += len(re.findall(pattern, content)) + + return count + + def _check_test_structure(self, content: str, test_file: Path) -> List[Dict]: + """Check if tests follow good structure patterns.""" + issues = [] + + # Check for Arrange-Act-Assert pattern (AAA) + lines = content.split('\n') + + # Look for test functions + test_functions = self._extract_test_functions(content, test_file.suffix) + + for func_name, func_body in test_functions: + # Check if test is too long (suggests poor structure) + func_lines = func_body.split('\n') + if len(func_lines) > 30: + issues.append({ + 'type': 'long_test', + 'severity': 'medium', + 'test': func_name, + 'message': f'Test "{func_name}" is {len(func_lines)} lines long. Consider breaking it down.' + }) + + # Check for multiple assertions in one test (might indicate poor isolation) + assertion_count = len(re.findall(r'assert|expect|should', func_body, re.IGNORECASE)) + if assertion_count > 5: + issues.append({ + 'type': 'multiple_assertions', + 'severity': 'low', + 'test': func_name, + 'message': f'Test "{func_name}" has {assertion_count} assertions. Consider splitting into focused tests.' + }) + + return issues + + def _extract_test_functions(self, content: str, extension: str) -> List[tuple]: + """Extract test function names and bodies.""" + functions = [] + + # Simple extraction for Python + if extension == '.py': + pattern = r'def (test_\w+)\s*\([^)]*\):\s*\n((?: .*\n)*)' + matches = re.finditer(pattern, content) + for match in matches: + functions.append((match.group(1), match.group(2))) + + # Simple extraction for JavaScript/TypeScript + elif extension in ['.js', '.ts']: + pattern = r'(test|it)\s*\([\'"]([^\'"]+)[\'"].*?\{([^}]*)\}' + matches = re.finditer(pattern, content, re.DOTALL) + for match in matches: + functions.append((match.group(2), match.group(3))) + + return functions + + def _check_tdd_patterns(self, content: str, test_file: Path) -> List[Dict]: + """Check for patterns that indicate TDD was followed.""" + issues = [] + + # Check for test-first indicators + # Red-Green-Refactor should result in: + # 1. Tests that clearly express intent + # 2. Minimal production code to make tests pass + # 3. Clear test names that describe behavior + + test_functions = self._extract_test_functions(content, test_file.suffix) + + for func_name, func_body in test_functions: + # Check for descriptive test names + if len(func_name) < 10 or not any(word in func_name.lower() for word in ['should', 'when', 'given', 'test']): + issues.append({ + 'type': 'poor_test_name', + 'severity': 'low', + 'test': func_name, + 'message': f'Test name "{func_name}" is not descriptive. TDD encourages behavior-focused names.' + }) + + # Check for setup/teardown patterns + if not any(keyword in content for keyword in ['setUp', 'beforeEach', 'before', 'setup', 'fixture']): + # Only flag if multiple tests exist + if len(test_functions) > 3: + issues.append({ + 'type': 'missing_setup', + 'severity': 'low', + 'message': 'No setup/fixture detected. Consider DRY principle in test arrangement.' + }) + break # Only report once per file + + return issues + + +def main(): + """Main entry point for the test validator.""" + if len(sys.argv) < 2: + print("Usage: validate_tests.py ") + print(" path: File or directory containing tests to validate") + sys.exit(1) + + path = sys.argv[1] + + if not os.path.exists(path): + print(f"Error: Path '{path}' does not exist") + sys.exit(1) + + validator = TestValidator(path) + results = validator.validate() + + # Output results as JSON + print(json.dumps(results, indent=2)) + + # Exit with appropriate code + if not results['valid']: + sys.exit(1) + else: + sys.exit(0) + + +if __name__ == '__main__': + main()