Spec Driven Development + Test Driven Development in practice using AI
Write a short spec, then ask an AI for one failing check and the smallest fix. A small calculator is only the practice case. You do not write the tests yourself.
1 Two habits, used together
Spec-driven development and test-driven development are often treated as rival methods. Each one answers a different question. The useful order is simple: decide what you are building, then prove one small piece at a time.
Spec-driven development means you write a short plan first. That plan is the spec. It lists what the program should do, what it should not do, and how you will know you are finished. You write this before you invent extra features.
Test-driven development means a check exists before the code that should satisfy it. A test is a tiny automatic check, like “12 plus 3 must come back as 15.” The first time it runs, it fails, because the feature is not there yet. Then only enough code is added to make that one check pass.
You do not write those checks yourself. You ask an AI, one spec line at a time. An AI here is a coding assistant you talk to in plain language. A prompt is the message you send it. Name the line, ask for a failing check only, wait until it fails, then ask for the smallest fix.
| Spec-driven development | Test-driven development | |
|---|---|---|
| What you do first | Write a short list of goals and limits | Ask the AI for one automatic check |
| Question it answers | What are we building, and what are we skipping? | Does this one piece still do what we said? |
| What goes wrong if you skip it | You add features nobody asked for | The code can be wrong and you notice late |
- SpecWrite what done looks like
- Ask failAsk for one check that is not satisfied yet
- Ask passAsk for only enough code for that check
- RepeatNext line from the spec
The order matters
If you are unsure what you are building, start with the spec. If a piece of code can quietly give the wrong answer, ask the AI for a failing check next. Keep each ask small. A prompt that says “write all the tests and the whole calculator” skips the method.
2 A small calculator as the practice case
I needed an example small enough to finish in one sitting. A calculator with add, subtract, multiply, divide, and a clear button is enough. Divide by zero should show the word Error. No memory keys. No extra science buttons. The calculator is only the practice case, so the prompts stay concrete. Finished page: thales.dev/calculator.
- Small enough that a long planning process would be more than this job needs
- One known special case, already named: dividing by zero
The sequence I used write the spec→ask for a failing add check→ask for the smallest add→repeat for the rest
3 Write the spec before any calculator code
I created a folder and asked the AI to install Vitest. Vitest is a program that runs the automatic checks. Then I stopped. I wrote a file called specs/spec.md: the problem, what is included, what is left out, and a few “when this happens, this should appear” lines. Later prompts point at this file.
- Create the project folder. Ask the AI to install Vitest. Stop there.
- Write the spec yourself: what we will build, what we will skip, and how we will know we are done.
- Tell the AI the math should live in its own file, away from the on-screen buttons. Do not ask it to write those functions yet.
The full spec file, specs/spec.md
# Calculator spec
## Rules (keep this small)
- Spec first. Do not invent features that are not listed here.
- Tests prove the engine. The UI only calls the engine.
- One failing test at a time. Write only enough code to pass that test.
- Out of scope is part of the contract. Ignore it on purpose.
## Problem
A visitor on thales.dev can open `/calculator` and do the four basic operations on two numbers, then clear the display.
## In scope
- Add, subtract, multiply, divide
- Clear
- Pattern: enter A, pick an operator, enter B, press `=`
- Divide by zero shows `Error`
## Out of scope
- Memory keys (M+, MR)
- Percent
- Parentheses or operator precedence chains
- Scientific functions
- Keyboard support
## Acceptance (EARS-style)
- When the user adds 12 and 3, the display shall show 15
- When the user subtracts 9 from 20, the display shall show 11
- When the user multiplies 6 by 7, the display shall show 42
- When the user divides 20 by 4, the display shall show 5
- When the user divides 10 by 0, the display shall show `Error`
- When the user presses Clear, the display shall reset to 0
## Engine contract
Pure functions in `calculator.js`. No DOM.
- `add(a, b)` returns a number
- `subtract(a, b)` returns a number
- `multiply(a, b)` returns a number
- `divide(a, b)` returns a number, or the string `Error` when `b` is 0
## Done when
- [ ] Spec written before production code
- [ ] Each operation has a failing test, then a passing test
- [ ] Divide-by-zero is covered
- [ ] UI uses the same engine
- [ ] Acceptance clicks match this file
That is the whole file. “EARS” is only a name for lines that say “when this happens, this should appear.” The “left out” list is part of the agreement. If it is not in the spec, I do not add it, even when it would be easy.
4 Ask for a failing check, then the smallest fix
The spec already said that 12 plus 3 should become 15. I did not type a test. I asked the AI for one automatic check of that line, and I told it to stop when the check failed. Add was missing, so the check received nothing useful. That failure is useful. It means the check is real, and the feature is not done yet.
- Ask for one check only. Name the spec line. Do not ask for the calculator code in the same message
- Stop when the check fails. A missing function name is a valid failure. Then send a second, smaller ask
- The second ask is only “make this one check pass.” Do not add subtract, multiply, divide, or buttons
First ask: one failing check
Read specs/spec.md. Do not write the calculator yet.
Write one automatic check only: 12 plus 3 must come back as 15.
Use Vitest. Leave add missing or empty so this check fails.
Stop when the check has failed. Do not fix it yet.
Second ask: only enough code to pass
The add check failed. That is what we wanted.
Write only enough code to make that one check pass.
Do not add subtract, multiply, or divide.
Do not build the on-screen buttons.
Do not ask for the whole calculator in one go
The spec already listed four operations. I still asked only for add. If you tell the AI to write every check and every function at once, you skip the fail-then-pass habit.
5 Repeat the same asks for the next operations
Subtract, multiply, and divide (when the second number is not zero) used the same two prompts. Name the next spec line. Ask for one failing check. Wait. Then ask for only enough code to pass it. Dividing by zero is a later ask, because the spec already separated that case.
| What I asked the AI to prove | Example numbers | Expected result |
|---|---|---|
| Subtract, one check only | 20 minus 9 | 11 |
| Multiply, one check only | 6 times 7 | 42 |
| Divide when the second number is not zero | 20 divided by 4 | 5 |
The same prompt, next spec line
Read specs/spec.md. Add is already proven.
Write one new automatic check only: 20 minus 9 must come back as 11.
Leave subtract missing so this check fails.
Stop when it has failed. Do not write subtract yet.
Each row is a pair of asks: fail first, then the smallest fix. If I had asked for a plain divide and stopped there, JavaScript would later return Infinity when someone divides by zero. Infinity means “this number has no end.” The spec asked for the word Error, so that case waited for the next prompt.
6 Divide by zero was already in the spec
I did not discover this problem by clicking buttons. The spec already said that 10 divided by 0 should show Error. I asked the AI to turn that sentence into one failing check. The current divide function returned Infinity. The check failed, which is what we want at this step.
Ask: one failing check for the Error line
The spec says 10 divided by 0 must show the word Error, not Infinity.
Write one automatic check for that line only.
Do not change divide until this check has failed.
Ask: only enough change to pass
The check failed because divide returned Infinity.
Change divide only enough so 10 divided by 0 returns the word Error.
Do not add extra features.
I did not type the fix. The AI added a simple rule: if the second number is 0, return the word Error. My part was the two prompts, then reading the report.
7 Ask for the on-screen buttons last
The automatic checks already proved add, subtract, multiply, divide, and the Error case. Only then did I ask the AI for the page you see. The buttons should not redo the math. They should call the four functions that already passed.
Ask: a screen that uses the proven functions
The math in calculator.js already has passing checks. Do not change those functions.
Build a simple page: a display that starts at 0, number keys, add, subtract, multiply, divide, equals, and clear.
When the person presses equals, call the matching function and show the result.
If divide returns the word Error, show Error on the display.
Try the finished page
That is the last prompt in this walkthrough. Open thales.dev/calculator to use it.
A warning: this is just a simple calculator. There is nothing special about the app. What matters here is the Spec-Driven Development and Test-Driven Development process.
8 References
These are the sources I used while writing this article. Some describe larger planning systems. Some describe the fail-then-pass habit. I ran that habit by asking an AI, not by typing the checks.
| Source | What I used it for |
|---|---|
| Tekk, SDD vs TDD | How the two habits fit in one sequence |
| GitHub Spec Kit | A larger spec-driven toolkit for wider features |
| Spec-first, spec-anchored, spec-as-source | Names for how tightly the spec sits on the work |
| Kent Beck, Canon TDD | One test, then make it pass |
| Uncle Bob, Cycles of TDD | Short rules for how little code to write at each step |
| Martin Fowler, TDD | A public explanation of fail, pass, then clean up |
| Hacker News on Spec Kit | When extra planning files help, and when they waste time |