| name | tdd |
| description | Implement a change using test-driven development with RSpec. Guides the specify-encode-fulfill workflow. |
| argument-hint | ["specification"] |
| disable-model-invocation | true |
Test-Driven Development
Initial Specification
$ARGUMENTS
The Specify-Encode-Fulfill Loop
I use something called "specify-encode-fulfill":
- Specify: Come up with the specifications for what you want to build
- Encode: Encode those specifications as automated tests (executable specifications)
- Fulfill: Write the code to fulfill the specifications
At a finer grain:
- Write a list of the specifications within scope of the current TDD session
- Encode one item in the list as an automated test
- Change the code just barely enough to make the current test failure go away. Avoid "speculative coding" - if we write more code than necessary to make the current test failure go away, we risk having code never exercised by any test
- Optionally refactor, but not before committing the behavior change. Never mix behavior changes with refactoring
- Until the list is empty, go back to #2
This follows Kent Beck's Canon TDD.
Clarifying Specifications
Before writing tests, follow this loop:
- Repeat my specifications back to me in your own words
- Ask me to confirm your articulation is correct or explain how it's wrong
- If confirmed, proceed to writing tests; otherwise use my response and go back to step 1
Specifications should take the form: "under scenario A, X happens; under scenario B, Y happens".
For guidance on designing good specifications, see test-design-review/SKILL.md.
Translating Specifications into Tests
Each scenario should map to an RSpec example group. If the specification is,
for example "when a test suite run's status is 'passed', its label says
'Passed'", then the test should look like this:
describe "#label" do
context "when status is 'passed'" do
it "returns 'Passed'" do
test_suite_run = TestSuiteRun.new(status: "passed")
expect(test_suite_run.label).to eq("Passed")
end
end
end
Here's an example of a BAD way to write such a test:
describe "#label" do
it "returns the correct value" do
test_suite_run = TestSuiteRun.new(status: "passed")
expect(test_suite_run.label).to eq("Passed")
end
end
Of course it returns the "correct" value. What else could we ever want from our
code? Never assert that a behavior "works correctly" or "works properly" or
"handles" certain scenario. What we want to specify in every scenario is what
the correct behavior is.
Workflow
- I invoke /tdd with a draft specification
- After back-and-forth, we agree on "final" specifications
- See if we need to "clean the kitchen before we make dinner". Refer to the
"Cleaning the Kitchen" section below.
- You write just one test (per Canon TDD)
- Show me the test and ask for approval before continuing
- Write the application code, show it to me, and ask for approval before
committing. Refer to the "Fulfilling Test Specifications" section below.
- I provide a new specification and we start over from step 2
Cleaning the Kitchen
Before you write a test, picture the test you're going to write and where
you're going to put it. Does the conceptual framework of this new behavior
we're about to add slot tidily into the conceptual framework of the area of the
code where we'll be adding it? If not, is there a reconceptualizing of the
current behavior that could be done in order to make the ending result more
conceptually elegant? If such a reconceptualizing is called for, suggest it to
the user. If the user approves, abandon the current change, get to a clean
working state, and, on a new branch, perform a refactoring. "Clean the kitchen
before you make dinner." Then pause and consult the user and we'll begin again.
Fulfilling Test Specifications
When writing the application code to fulfill a certain specification, write
ONLY ENOUGH CODE to make the current test failure go away. Never use "defensive
coding". Defensive coding is almost always just speculative coding, which means
code that's added without justification or feedback. Once you've written the
test, invoke a separate subagent with the /test-design-review command to
scrutinize your test code. Then invoke another subagent with the
/software-design-review command to scrutinize your application code.
Don't Be Sloppy
This kind of thinking is bad:
That failure is pre-existing (unrelated to our change โ it's in send_results).
Our 6 new + existing specs pass. Want me to commit and push?
We don't make dinner in a dirty kitchen. If we discover a pre-existing failure,
the right move is to pause, stash our changes, fix the pre-existing failure,
then resume.
Don't Be Lazy
This is stupid and bad:
Bundler version mismatch. The task agent uses bundler 2.5.22 but the system has
a different version. The tests can't run here. But the changes are
straightforward โ removed re-tagging and made preload_app_image return the
image name.
Don't abandon tests immediately upon encountering the slightest difficulty.