TL;DR

Understanding Test-Driven Development (TDD) and what makes testing valuable for developers

Hi, I'm Gunner (Jae-hoon Jung), a backend developer building hotel operations software at ONDA.

In Part 1, we explored different perspectives on software testing — including tools like BDD and TDD.

Sometimes TDD and testing get conflated, which muddles what each actually means. In this post, we'll dig into what TDD really is, examine the debates that erupted among developers, and work toward understanding what good software testing looks like for developers.

1. What Is Test-Driven Development (TDD)?

Let's start with the concept. TDD is a software design technique introduced in Kent Beck's ⌜Test-Driven Development By Example⌟ (hereafter TDDBE). It's a method where you let tests drive development.

*Many see TDD as a testing method, but I think it's closer to a design method.

The TDD process in TDDBE is simple:

  1. Write a failing test first
  2. Write the minimum code to pass it as quickly as possible
  3. Refactor

The book calls this the "Red-Green-Refactor cycle" — red for failing tests, green for passing.

Smalltalk SUnit's TestRunner results
Smalltalk SUnit's TestRunner results

In TDDBE, Kent develops a Money class, implementing currency units step by step. For example, the test passes when using dollars ($), but fails when you add another currency — so you create new classes to handle it.

By writing failing tests first, the Red-Green-Refactor cycle doesn't just drive the design of the Money class — the automated tests also act as a safety net that verifies existing features when you add new ones.

Another key point: Kent takes tiny steps in TDDBE. Later, in an interview, he clarified that this doesn't mean you must always take tiny steps. The goal is to help you develop the sense to take small steps when you're unsure, and leap forward when you're confident.

In short, the core of TDD in TDDBE is the Red-Green-Refactor cycle — using it to build good design and automated tests.

Test-Driven Development by Kent Beck, November 2002
Test-Driven Development by Kent Beck, November 2002

2. TDD Sparks Debate Among Developers

After TDD emerged, developers split into camps. The main divide: "You should develop with TDD" vs. "TDD doesn't work in real projects."

The "You should use TDD" camp emphasized TDD's benefits — good design through Test-First, automated tests built alongside development. The "TDD doesn't work" camp disagreed with Test-First as a design method.

(1) Why Did Kent Advocate TDD?

Before diving deeper, let's understand why Kent advocated TDD in the first place.

Kent primarily used Smalltalk. He applied XP (eXtreme Programming) on a famous project called C3 using this language. Smalltalk runs on a VM (Virtual Machine). Its entire development environment is essentially one IDE (Integrated Development Environment), and source code is stored as a single integrated image.

GemStone Smalltalk platform
GemStone Smalltalk platform

Smalltalk is object-oriented by design. It had the right environment and characteristics for TDD:

  • You can run code directly in the code browser
  • Designed around many small objects sending messages to each other
  • Smalltalk originated the MVC pattern, but didn't separate files by layer like modern Java projects — it stored all source code as one image
  • xUnit's ancestor is SUnit

Most Java Spring projects work differently:

  • You need to spin up the Spring context to run code
  • Relatively fewer class divisions compared to Smalltalk
  • Clear layer separation (influenced by DDD, hexagonal architecture, etc.), with files split across layers — expanding the scope of what needs testing
  • JUnit is a port of SUnit

In Smalltalk, you can't navigate "file → class" like in Java. Heavy object interaction made automated tests necessary to verify behavior. Conversely, Smalltalk was an excellent environment for running unit tests fast and getting immediate feedback.

(2) "TDD Is Dead. Long Live Testing."

TDD emerged in the early 2000s, sparking debate. Developers split into groups that embraced it and groups that didn't. Then in 2014, DHH (David Heinemeier Hansson) — creator of Ruby on Rails (*a full-stack web framework that influenced nearly every full-stack framework since) — posted a provocatively titled piece on his blog: "TDD Is Dead. Long Live Testing." The TDD wars began.

DHH's main points:

  • TDD has value, but it's become a dogmatic religion

  • TDD's focus on unit tests sometimes brings unnecessary complexity (e.g., mocks)

      ◦ Complex structures of intermediary objects between layers
    
      ◦ Avoiding DB or I/O in tests → system tests treated as bad practice
    
      ◦ A jungle of service objects, command patterns, and worse → patterns and object design solely for testing
    
  • TDD might drive design, but it didn't match my design approach

  • I rarely do unit tests in the traditional sense — mocking all dependencies, thousands of tests finishing in seconds

      ◦ Not considered good tests in Rails
    
      ◦ I test ActiveRecord models directly — create fixtures, hit the DB directly
    
      ◦ At the top layer, I have controller tests, but I prefer replacing them with high-level system tests using tools like Capybara
    
  • I'm not saying everyone should work like me, but let's stop treating TDD as the only path to good development

(3) Kent Beck vs. DHH — Their Views on TDD

The post stirred backlash from TDD supporters, igniting an online firestorm.

Eventually, Martin Fowler (famous for Refactoring) moderated an online debate between Kent Beck and DHH. Here's a summary of their conversation:

Defining unit tests in TDD

  • DHH: What's the definition of a unit test in TDD? Must you use mocks for unit tests? Also, the Red-Green-Refactor cycle worked for me in some ways, but overall it didn't fit.

  • Kent: Programmers have always tested (in some way). Working with Smalltalk, I automated it. I tried Test-First — it worked great for me.

  • Common ground: TDD has value, but it varies by taste and project characteristics.

  • DHH: Many people make bad trade-offs using heavy mocks. Why accept this cost?

  • Kent: Using mocks is a trade-off — I agree. If you can build a fast feedback loop with real objects, use real objects. I rarely used mocks doing TDD (Smalltalk doesn't layer like Java, so you test directly without mocks).

    Also, when you modify production code, you have to update test code. If production code changes a lot, you have to change all the corresponding mocks. Tests make refactoring easier, but mocks make refactoring harder — that concerns me.

  • DHH: Layered architecture with tons of mocks has too much indirection and complexity.

  • Kent: That's not TDD's fault.

  • Martin: Right. That stems from hexagonal architecture's core principle: "thorough isolation from the environment."

  • DHH: Agreed. But I often see people emphasize separation as a goal in itself — because TDD requires unit tests.

    Hexagonal architecture advocates say you could run the domain model as a terminal app or command-line tool. I want to ask them: will that ever happen?

    Same with repositories. They say you can swap a DB implementation for an in-memory DB or web service — will that ever happen?

  • Kent: Again, that's not a TDD problem — it's about design and how fast you get feedback. Design issues need careful design consideration.

  • Common ground: Fast feedback matters. TDD can't replace QA.

Should you TDD all code? Can there be too many or unnecessary tests?

  • Kent: I don't always do TDD either. People don't write test code to pay a cost — they write it to gain sufficient confidence.

  • Martin: Over-testing definitely exists. If I comment out this line, will a test break? The key is testing cases that should break. Places like getters are trustworthy — no need to test them.

・ Can't change code confidently? → Not enough tests, or not good tests ・ Changing tests is harder than changing code? → Too many tests

When is TDD useful?

  • Kent: TDD breaks problems into small units and builds confidence.
  • Martin: Some domains suit TDD, some don't.
  • DHH: I agree some domains suit TDD. In my experience, it often didn't fit MVC web applications. But even in domains where TDD doesn't fit, automated self-testing is essential. That's the key value in TDD.

The conclusion: TDD is a useful tool, but partly a matter of taste — and not suited to every domain.

💡 On TDD being a matter of taste, let's also look at what Rich Hickey (creator of Clojure) said.

Rich called this "guardrail programming""They say you can make changes because you have tests. Who does that? Who drives by bouncing off guardrails?" — criticizing TDD's Red-Green-Refactor cycle. He wasn't criticizing TDD itself, but arguing that the ability to analyze programs logically matters more (thoughtful analysis should inform program design).

In other words, developing with TDD or having all tests pass doesn't make a good program. This echoes his earlier jabs at type checkers.

Rich acknowledged the need for automated self-testing, but after TDD advocates attacked him, he said: "If criticism that your tool or methodology doesn't fit a specific purpose feels like an attack, you need to relax." Methodology is just a tool.

(4) What the Ongoing TDD Debate Reveals

Why does the TDD debate continue, even if less actively than before?

Looking closely, many benefits attributed to TDD don't actually come from TDD itself. For example:

  • Must you do TDD to get automated self-testing? No.
  • Must you do TDD to get regression tests? No.
  • Must you do TDD to get good design (*what "good design" means is another discussion)? No.
  • TDD expresses specs clearly in code — but is TDD the only way? No.

TDD is a design method following the Red-Green-Refactor cycle. It has value, but it doesn't fit everyone or every project. Also, as Kent noted in the debate, mocks aren't related to TDD. We need to clearly separate the value of TDD (a design technique) from the value of testing.

3. What Makes a Good Test for Developers?

Before considering what makes a good test, we must acknowledge: testing is a trade-off. Creating and maintaining tests consumes developer resources.

Tests help development, but as Martin Fowler said, there's "not enough" and "too much." So what's a good test for developers?

(1) Code to Test vs. Code Not to Test

A good test covers brittle code — code prone to breaking.

Brittle code

  • Code referenced in many places
  • Core logic code
  • Code likely to change in the future
  • Code that caused issues in QA or production

Spotting these requires more than unit tests. This is where BDD helps. Thinking through Given/When/Then naturally reveals how an object relates to others.

Conversely, it's important to decide some areas are trustworthy enough to skip testing.

Robust code

  • Code used in few places
  • Clear, simple code
  • Library functionality

But there's no absolute rule for what not to test. Say your team typically skips testing library code. If a library updates frequently with changing interfaces or behavior, testing code that uses that library becomes worthwhile.

Deciding what not to test depends on your context. Think it through.

(2) Mocks — Must You Use Them?

Testing debates often fixate on mocks.

Methodologies like TDD fundamentally talk about unit tests. Unit tests should quickly verify a unit's behavior in isolation. By that definition, unit tests should mock dependencies on other layers.

That's reasonable, but in the TDD debate, DHH emphasized mock harm and Kent said he rarely used mocks doing TDD. Kent even worried mocks could make refactoring harder.

Fundamentally, mocks enable "fast testing in isolation."

But using mocks isn't a rule. It's just one tool for structuring tests.

  • Isolation: In object-oriented systems where multiple objects collaborate, is an isolated unit test with mocks really a good test?
  • Speed: If using real layer objects (instead of mocks) doesn't slow things down, is using real objects a problem?

(3) Automated Self-Checking Regression Tests

Another hallmark of good tests: automated self-checking regression tests. Let's break down each word:

  • Automated: So obvious it's easy to overlook, but crucial. If running tests requires manual setup, you'll avoid running them.
  • Self-checking: Functionality should verify itself. If a human has to judge test results, that's (obviously) work.
  • Regression: Confirm that features that worked before still work now.

Automated self-checking regression tests can't guarantee all code is safe. But if tests cover the parts you've thoughtfully decided need testing, you can feel confident about those parts.

One more thing: while TDD naturally produces automated self-checking regression tests, that's not TDD's goal. You can create automated self-checking regression tests without TDD.

TDD is one useful design method. What matters most is the developer's ability to identify brittle code and write precise automated self-tests.

The truth is, developers tested long before these debates. They printed values to logs, screens, printers — checking if outputs matched expectations. Sometimes they wrote shell scripts to run programs and verify outputs visually or programmatically. Kent formalized this with TDD and SUnit, sparking much debate. But amid the controversy, no one disputed the value of automated self-testing — because skilled developers have always tested, one way or another.

References

  1. Translated edition: http://www.yes24.com/Product/Goods/12246033, Original: Test Driven Development: By Example: Beck, Kent: 8601400403228: Amazon.com: Books

  2. Chrysler Comprehensive Compensation System - Wikipedia

  3. JUnit Was Born on a Plane! – Tesla Tales (wordpress.com)

  4. TDD is dead. Long live testing. (DHH)

  5. "TDD Is Dead" - DHH's post (sangwook.github.io)

  6. Studying TDD. Is TDD dead? (junho85.pe.kr)

  7. Translation project: Is TDD dead? (tistory.com)

  8. "Simple Made Easy" - Rich Hickey (2011) (youtube)


⚠️ All rights reserved. Cite this piece as 'ONDA (온다)' when quoting.