The 3 Levels of Resolution of TDD
View original- best
Summary (TL;DR)
The article explains TDD as a spectrum with three levels of resolution rather than a binary practice. At high resolution, TDD is textbook-style: write failing tests, implement minimal code, refactor, ideal for unclear domains. Middle resolution involves reasoning through steps on a whiteboard without running tests, useful for collaboration. Low resolution is a debugging instinct where errors guide next steps. The core discipline is a feedback loop, just with different shapes. The author used this framework to help three teams stop arguing about whether to use TDD and instead discuss what resolution fits the problem. Not every problem needs high-level TDD, but high resolution helps preserve reasoning in code, reducing bus factor. The middle level covers most daily work.
The 3 Levels of Resolution of TDD
Most arguments about TDD are about whether to do it or not.
That’s the wrong question.
The useful question is what you’re looking at when you apply the practice:
- A test file
- A whiteboard marker
- A console full of stack traces.
I started using the phrase “levels of resolution” after watching the same argument play out across 3 teams I coached years ago. One had tried London-school TDD and abandoned it, another wrote tests after the fact and called it TDD because "it has tests", and a third refused to discuss testing at all because a previous lead had made the practice religious and nobody understood what it meant.
All 3 were stuck on the same dogmatic choice:
Either the test comes first, or it isn’t TDD.
Ok, so what does "resolution" actually mean for a practice like TDD? Well… the same thing it means for the map on your phone!
Street view shows individual buildings, one zoom out gives you the suburb, and another puts the whole city in the frame. The information changes at every level, but the geography underneath doesn’t move, only how much you see it.
TDD works the same way.
The discipline remains the same, top to bottom, and the level you’re in decides how much of it ends up written down as code.
Take one small problem through all 3 levels: adding money in JavaScript in most English-speaking countries. The most common (and naive) practice is to use JS Numbers. A subtotal of 16.90 plus shipping of 4.20 comes out as 21.099999999999998 and the sum doesn’t pass a strict check against 21.10.
Resolution 3: High
At the highest resolution, TDD is the practice the textbooks describe: write a failing test, write the minimum code that passes it, then refactor. This level fits when the problem domain isn’t clear yet and the test suite is the instrument that reveals the shape of the design, one step at a time.
Resolution 3: Write a failing test, write the minimum code that passes it, then refactor
For the money problem, the loop starts with basic tests (see this post for the details from 0 to 1) and eventually arrive at a stage where you need to write a test that states the drift out loud:
test('adds two amounts without drifting', () => {
expect(add('$16.90', '$4.20')).toBe('$21.10')
})
// red: $21.099999999999998const add = (a, b) => format(cents(a) + cents(b))
// green, next test: rounding when splitting a billEach red is a small predictable failure and the design of the money type comes out of answering them. TDD: Show Me The Code builds a full money data type out of the same loop, test by test. Check it out for a full highest resolution example.
Resolution 2: Middle
At the middle resolution, TDD is a mind/collaboration practice. You don’t open the test runner, you reason through the smallest number of steps before writing anything, on a whiteboard or out loud with a pair on in a scratchpad. The thinking is the same, a search for the smallest move that gets to the next known state.
Resolution 2: A mind collaboration practice
The whiteboard version of the same problem fits in 5 lines.
totals drift by a cent
parse "$16.90" -> 1690 cents at the boundary
add plain integers in the middle (cents / minor units)
format 2610 -> "$26.10" on the way out
floats live at the edges, if anywhereNo runnable test yet, but every line is a step that can fail on its own, and that’s what makes it the same discipline.
Resolution 1: Low
At the lowest resolution, TDD is a debugging instinct. Say a stack trace appears, the smallest possible change turns the error it into a different one, and the new error becomes the next step. There’s no test and no whiteboard, but the work is still a hypothesis tested against feedback.
Resolution 1: A hypothesis tested against feedback
Here’s the same problem from the console.
> const subtotal = 16.90, shipping = 4.20
undefined
> subtotal + shipping
21.099999999999998
> (subtotal + shipping).toFixed(2)
'21.10'
> (subtotal + shipping).toFixed(2) + 5
'21.105'
> Number((subtotal + shipping).toFixed(2)) + 5
26.1The toFixed call returns a string instead of a number, the next expression concatenates the "5" onto the end of it, and that’s the different error that becomes the next step so we can cast appropriately.
The common thread across all 3 is the feedback loop.
A test runner works at the top, in the middle it runs inside your head against the constraints you know, and at the bottom an error message does the job.
The discipline stays the same at every level of resolution. The only thing that changes is the shape of the feedback loop.
When I described the 3 levels to the most resistant of those teams, the engineer who’d refused to discuss TDD said, “I already do the bottom ones.”
He did. He was doing TDD all along and just didn’t have a name for it!
That recognition completely changed the room.
Once the team saw TDD as a spectrum of thinking instead of a black/white switch, the argument moved from “should we do TDD” to “what resolution fits this problem” or "what are some specific concrete cases we can think of"?
Infrastructure-as-code sits near the bottom, where the loop is a deployment error rather than a test suite. A pure function that parses domain objects, like the money type above, sits at the top, where a test suite maps the logic out faster than reasoning about it alone (and leaves that reasoning behind for the next person).
The middle is where most of the day-to-day work lives. You think first, find the smallest useful step, and hold the structure in your head until the code catches up.
Not every problem requires a dogmatic high resolution TDD approach.
At highest resolution, the reasoning behind a change lives in code. Anyone can read the tests and follow the path to the answer, and when the author leaves the team, the tests don’t leave with them. The bus factor drops.
At lowest resolution, the reasoning lives in one person’s head. That’s fine for a quick debugging session, and it’s a problem when it becomes the default way to build features.
The levels gave the team a shared vocabulary for a conversation that used to stall. “This problem deserves higher resolution” invites a discussion about the problem, and “you should write more tests” invites an argument about the practice.
The intent is the same in both, but only one of them ends with the team looking at the code.
If there's one thing I learned about this: dogmatic approaches hurt progress; being aware of the exceptions demonstrate expertise.
If you liked this, you might like readplace.com, built for exactly this kind of reading.
Thanks for reading. If you have some feedback, reach out to me on LinkedIn, Reddit or by replying to this post.