Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Practice strict red-green-refactor test-driven development — write one failing test first, make it pass with the minimum code, then refactor under green, with worked cycles in Jest and pytest, AAA structure, and behavior-based test naming.
.claude/skills/pramoddutta-tdd-patterns/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 13 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✓→✓ | = Same ✓ | 69% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 64% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 90% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 223% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 87% | 0% |
This skill makes an AI agent develop features test-first: write exactly one failing test, watch it fail for the right reason, write the minimum production code to pass, then refactor while green. It enforces the discipline most "TDD" sessions skip — never writing production code without a failing test demanding it. Trigger it when the user asks for TDD, test-first development, or when implementing new logic in a codebase that already has a test runner wired up.
src/.rejects expired coupons at the boundary minute tells the next reader the rule; test_coupon_3 tells them nothing.Feature: a PriceCalculator that applies tiered bulk discounts.
RED — write the smallest failing test:
typescript// src/price-calculator.test.ts import { describe, expect, it } from '@jest/globals'; import { calculateTotal } from './price-calculator'; describe('calculateTotal', () => { it('returns unit price times quantity with no discount under 10 units', () => { // Arrange const unitPrice = 4.0; const quantity = 3; // Act const total = calculateTotal(unitPrice, quantity); // Assert expect(total).toBe(12.0); }); });
bashnpx jest price-calculator # FAIL — Cannot find module './price-calculator' <- failing for the RIGHT reason
GREEN — minimum code, no speculation:
typescript// src/price-calculator.ts export function calculateTotal(unitPrice: number, quantity: number): number { return unitPrice * quantity; }
RED again — the next test forces the discount rule:
typescriptit('applies a 10 percent discount at 10 units or more', () => { expect(calculateTotal(4.0, 10)).toBe(36.0); // 40 - 10% }); it('applies a 20 percent discount at 50 units or more', () => { expect(calculateTotal(2.0, 50)).toBe(80.0); // 100 - 20% });
GREEN:
typescriptexport function calculateTotal(unitPrice: number, quantity: number): number { const subtotal = unitPrice * quantity; if (quantity >= 50) return subtotal * 0.8; if (quantity >= 10) return subtotal * 0.9; return subtotal; }
REFACTOR — under green, extract the tier table:
typescriptconst DISCOUNT_TIERS: ReadonlyArray<{ minQty: number; multiplier: number }> = [ { minQty: 50, multiplier: 0.8 }, { minQty: 10, multiplier: 0.9 }, { minQty: 0, multiplier: 1.0 }, ]; export function calculateTotal(unitPrice: number, quantity: number): number { const tier = DISCOUNT_TIERS.find((t) => quantity >= t.minQty)!; return unitPrice * quantity * tier.multiplier; }
Run the suite after the refactor. Still green, behavior unchanged, structure improved. That is one complete cycle.
Feature: a password strength validator, driven boundary-first.
python# tests/test_password_policy.py import pytest from app.password_policy import validate class TestValidate: def test_rejects_passwords_shorter_than_12_chars(self): # Arrange / Act result = validate("Short1!aaaa") # 11 chars # Assert assert result.ok is False assert "at least 12 characters" in result.errors def test_accepts_a_12_char_password_meeting_all_rules(self): result = validate("Sturdy-Pass1") # exactly 12 assert result.ok is True assert result.errors == []
bashpytest tests/test_password_policy.py -x # ModuleNotFoundError: No module named 'app.password_policy' <- correct red
Minimum green:
python# app/password_policy.py from dataclasses import dataclass, field @dataclass class Result: ok: bool errors: list[str] = field(default_factory=list) def validate(password: str) -> Result: if len(password) < 12: return Result(ok=False, errors=["at least 12 characters"]) return Result(ok=True)
Next red drives the remaining rules — and parametrize keeps each rule one logical test:
python@pytest.mark.parametrize( ("password", "missing"), [ ("alllowercase-12", "an uppercase letter"), ("ALLUPPERCASE-12", "a lowercase letter"), ("NoDigitsHere-Ab", "a digit"), ], ) def test_reports_each_missing_character_class(self, password, missing): result = validate(password) assert result.ok is False assert missing in result.errors
Green, then refactor the rule checks into a table of (predicate, message) pairs — same move as the discount tiers above.
When one example lets you fake it (return 36.0), add a second example with different inputs. Two data points force the general implementation; that is exactly when to generalize, not before.
Every test reads as three blocks separated by blank lines. One Act per test. If you need a second Act, you need a second test.
Before starting, jot the behaviors as comments; convert one at a time into a real failing test:
typescript// TODO test list — price-calculator // [x] no discount under 10 units // [x] 10% at 10+ // [x] 20% at 50+ // [ ] rejects negative quantity with RangeError // [ ] rounds to 2 decimal places (0.1 + 0.2 money bugs)
Write the boundary test (exactly 10 units, exactly 12 chars) before the comfortable middle. Off-by-one bugs live at boundaries; TDD that skips them certifies nothing.
jest price-calculator --watch, pytest -x -k password); run the full suite before commit.git commit after each cycle gives you a bisectable history and a free undo for failed refactors.expect(repo.save).toHaveBeenCalled() as the only assertion). Verify observable outcomes; interaction-only tests pass while behavior is broken.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 22,693 | 22,502 | -1% | 1 | 1 | 0% | 3,323 | 5,629 | +69% | 0 | 0 | — |
case-02 | pass→pass | 19,000 | 14,974 | -21% | 1 | 1 | 0% | 2,327 | 3,825 | +64% | 0 | 0 | — |
case-03 | pass→pass | 18,753 | 17,327 | -8% | 1 | 1 | 0% | 2,169 | 4,121 | +90% | 0 | 0 | — |
case-04 | pass→pass | 6,354 | 5,437 | -14% | 1 | 1 | 0% | 928 | 2,993 | +223% | 0 | 0 | — |
case-05 | pass→pass | 23,307 | 22,246 | -5% | 1 | 1 | 0% | 2,484 | 4,633 | +87% | 0 | 0 | — |
case-06 | pass→pass | 13,070 | 19,815 | +52% | 1 | 1 | 0% | 1,879 | 4,523 | +141% | 0 | 0 | — |
case-07 | pass→pass | 21,917 | 19,356 | -12% | 1 | 1 | 0% | 2,499 | 4,359 | +74% | 0 | 0 | — |
case-08 | pass→pass | 16,808 | 7,175 | -57% | 1 | 1 | 0% | 1,727 | 3,170 | +84% | 0 | 0 | — |
case-09 | pass→pass | 18,823 | 12,350 | -34% | 1 | 1 | 0% | 2,006 | 3,822 | +91% | 0 | 0 | — |
case-10 | pass→pass | 14,263 | 15,324 | +7% | 1 | 1 | 0% | 2,113 | 3,840 | +82% | 0 | 0 | — |
case-11 | pass→pass | 18,205 | 13,586 | -25% | 1 | 1 | 0% | 2,090 | 3,539 | +69% | 0 | 0 | — |
case-12 | pass→pass | 5,076 | 8,669 | +71% | 1 | 1 | 0% | 734 | 2,750 | +275% | 0 | 0 | — |
case-13 | pass→pass | 12,455 | 12,050 | -3% | 1 | 1 | 0% | 1,029 | 3,134 | +205% | 0 | 0 | — |
case-14 | pass→pass | 19,808 | 17,189 | -13% | 1 | 1 | 0% | 2,184 | 4,162 | +91% | 0 | 0 | — |
case-15 | pass→pass | 9,085 | 12,982 | +43% | 1 | 1 | 0% | 1,402 | 3,335 | +138% | 0 | 0 | — |
case-16 | pass→pass | 19,303 | 18,011 | -7% | 1 | 1 | 0% | 2,130 | 4,124 | +94% | 0 | 0 | — |
case-17 | pass→pass | 15,390 | 13,126 | -15% | 1 | 1 | 0% | 2,251 | 4,083 | +81% | 0 | 0 | — |
case-18 | pass→pass | 14,997 | 14,966 | -0% | 1 | 1 | 0% | 2,285 | 3,692 | +62% | 0 | 0 | — |
case-19 | pass→pass | 18,374 | 14,257 | -22% | 1 | 1 | 0% | 2,004 | 3,603 | +80% | 0 | 0 | — |
case-20 | pass→pass | 11,744 | 16,163 | +38% | 1 | 1 | 0% | 1,744 | 3,928 | +125% | 0 | 0 | — |
case-21 | pass→pass | 21,813 | 22,165 | +2% | 1 | 1 | 0% | 2,726 | 4,845 | +78% | 0 | 0 | — |
case-22 | fail→fail | 32,264 | 25,239 | -22% | 1 | 1 | 0% | 4,433 | 5,449 | +23% | 0 | 0 | — |
case-23 | pass→pass | 18,506 | 16,816 | -9% | 1 | 1 | 0% | 2,085 | 4,046 | +94% | 0 | 0 | — |
case-24 | pass→pass | 14,105 | 17,809 | +26% | 1 | 1 | 0% | 1,853 | 4,068 | +120% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of 0 percentage points is the difference between those two pass rates over the 24 comparable cases.
Other measured skills in the registry, with their headline benchmark lift.