| name | ijfw-tdd |
| description | RED-GREEN-REFACTOR enforcement. Use when implementing a feature or bugfix before writing implementation code. Trigger: tdd, test first, red green refactor, /ijfw-tdd |
| since | 1.5.0 |
IJFW TDD -- RED-GREEN-REFACTOR
Write the test first. Watch it fail for the right reason. Write the minimum code to make it pass. Then improve structure without changing behavior.
Iron law: No production code without a failing test first. If you wrote code before the test, delete it. Implement fresh from the test.
This skill enforces three distinct moves. Each move has a green light that you must hit before advancing. Skipping a green light is not "pragmatic" -- it is the bug.
Move 1: RED -- write a test that fails for the RIGHT reason
Write ONE minimal test for ONE behavior. Use a descriptive name. Use real code (no mocks unless unavoidable).
Before running the test, write down the failure message you EXPECT to see. Example: "AssertionError: expected 'Email required', got undefined" or "AttributeError: module has no attribute 'retryOperation'".
Then run it:
npm test path/to/test.ts
Paste the actual failure into your scratch. Compare against what you predicted.
Green light to advance: the test FAILS, and the failure message is the one you predicted (an assertion failure on the intended behavior, or a "missing symbol" error if the function does not exist yet).
Red flags that block advancing:
- Test passes immediately -- you are testing existing behavior. Rewrite.
- Test errors on a typo, import, or syntax issue -- fix and re-run until it fails meaningfully.
- Failure message surprises you -- you do not yet understand what you are building. Stop and think before writing implementation.
Move 2: GREEN -- write the minimum code to pass
Write the simplest implementation that turns the failing test green. No extra parameters. No "while I am here" cleanup. No anticipating tomorrow's tests. YAGNI.
Run the test. Paste the pass output. Run the full suite -- nothing else may regress.
Green light to advance: target test passes, full suite passes, output is pristine (no warnings, no stack-trace noise, no skipped tests you forgot about).
Red flags that block advancing:
- You added a feature the test does not cover -- delete it.
- Other tests broke -- fix now, do not defer.
- You changed the test to make it pass -- revert. The test defines the contract; the code is what bends.
Move 3: REFACTOR -- improve structure WITHOUT changing behavior
Now and only now: rename, extract helpers, remove duplication, tighten types, collapse branches. The behavior must stay identical.
Run the FULL test suite after each meaningful change. Not at the end -- after each change. If you cannot remember which change broke green, you waited too long.
Green light to advance to the next test: every refactor kept the suite green. No new behavior was added under the cover of "cleanup."
Red flag: you find yourself wanting to add a feature mid-refactor. Stop. Commit the refactor. Start a new RED test for the new feature.
Anti-patterns (and WHY each is wrong)
- Testing the mock, not the code. Asserting
screen.getByTestId('sidebar-mock') proves the mock loaded, not that the component works. The test passes for the wrong reason and gives false confidence.
- Tautological assertion.
expect(result).toBe(result) or expect(fn()).toEqual(fn()). Always green, proves nothing, exists only to push coverage numbers.
- Coverage-driven test that asserts nothing. Calls the function, never checks the return value. The line counter goes up; the bug count does too.
- Skipping RED because "I know what it will fail on." You don't. Half the time the test passes immediately (you tested the wrong thing) or errors on a typo (you tested nothing). Watching it fail is the only proof the test can ever fail.
- Test-only methods on production classes. A
destroy() that exists "for tests" is production API that real callers will eventually invoke. Put cleanup helpers in test utilities.
- Incomplete mocks. Mocking only the fields your test reads hides the structural contract. Downstream code that consumes other fields fails silently. Mirror the real shape completely or do not mock at all.
Multi-domain examples
TDD is not a software-only discipline. It applies anywhere you have a falsifiable claim about what "done" means.
- Book continuity -- writing chapter 7? Before drafting, write the continuity check: "By end of ch7, Maya knows about the letter AND has not yet met Olu." Draft until the check holds. Re-run the check after every revision.
- Campaign metrics -- launching a landing page? Before shipping, define the test: "Launch worked = 100 signups in 7 days AND CAC < $12 AND bounce < 60%." Anything else is "I think it went OK," which is a manual ad-hoc test.
- Design-system contrast -- before picking a palette, write the test: "Every text/background pair scores WCAG AA (4.5:1) or better, verified by axe-core." Palette choices that fail the test never enter the system.
- Onboarding copy -- before writing the welcome email, write the test: "A reader who has never used the product can name the first action to take in under 10 seconds." Read the draft to a non-user and time them.
In every domain, the move is the same: write the falsifiable claim FIRST, watch reality fail to meet it, then build until the check holds.
When stuck
| Problem | Move |
|---|
| Don't know how to test it | Write the wished-for API. Write the assertion. Ask for help. |
| Test feels too complicated | The design is too complicated. Simplify the interface. |
| Must mock everything | Code is too coupled. Use dependency injection. |
| Test setup is huge | Extract helpers. Still huge? The design is wrong. |
Final rule
Production artifact exists -- a function, a chapter, a metric, a palette -- a test failed for it first. Otherwise: not TDD. Delete and restart.