The Three Laws of TDD
1. Write no production code until you have a failing test. 2. Write no more test than is sufficient to fail. 3. Write no more production code than is sufficient to pass the test. The cycle is: Red → Green → Refactor. Tests written after code are often incomplete — they test what you happened to write, not what the code should do.
// TDD cycle for a discount calculator
// ── Red ─────────────────────────────────────────
function test_no_discount_for_regular_users():
calc = new DiscountCalculator()
assert calc.apply(100.0, "regular") == 100.0 // fails — class doesn't exist
// ── Green ───────────────────────────────────────
class DiscountCalculator:
function apply(price, tier):
return price // simplest code to pass
// ── Refactor ────────────────────────────────────
// Next test drives the next behavior:
function test_10_percent_off_for_premium_users():
calc = new DiscountCalculator()
assert calc.apply(100.0, "premium") == 90.0
The F.I.R.S.T. Rule
Clean tests are Fast (run in milliseconds — no DB, no network), Independent (no test sets up state for another), Repeatable (same result in any environment), Self-validating (pass or fail — no human reading of output), Timely (written just before the production code). A slow, order-dependent test suite is a test suite developers stop running.
Not FIRST — slow, order-dependent, real DB
class TestUserService:
function test_create_user():
db = RealDatabase.connect("localhost:5432") // slow!
service = new UserService(db)
user = service.create("Alice", "a@t.com")
savedUserId = user.id // ← shared state!
function test_find_user():
user = service.find(savedUserId)
assert user.name == "Alice" // depends on prev test
FIRST — fast, isolated, uses a fake
class TestUserService:
function setup():
db = new FakeDatabase() // fast, in-memory
service = new UserService(db)
function test_create_returns_user_with_given_name():
user = service.create("Alice", "a@t.com")
assert user.name == "Alice"
function test_find_returns_null_for_unknown_id():
result = service.find("unknown-id")
assert result == null
One Assertion Per Test
Each test function should verify one concept. Multiple assertions per test mean multiple failure modes — when it fails, you don't know which concept broke. Use descriptive test names as documentation: test_inactive_users_cannot_place_orders tells you the rule being tested. Future developers will read tests to understand behavior, not source code.
One test, six assertions, six failure points
function test_user():
user = new User("alice", "alice@test.com")
assert user.name == "alice"
assert user.email == "alice@test.com"
assert user.isActive == true
user.deactivate()
assert user.isActive == false
user.setRole("admin")
assert user.role == "admin"
One concept per test
function test_user_is_active_by_default():
user = new User("alice", "alice@test.com")
assert user.isActive == true
function test_user_becomes_inactive_after_deactivation():
user = new User("alice", "alice@test.com")
user.deactivate()
assert user.isActive == false
function test_user_role_can_be_set():
user = new User("alice", "alice@test.com")
user.setRole("admin")
assert user.role == "admin"
Code Challenge
Split the God Test — this test has 5 different concepts. Split each into its own function with a descriptive name that reads as a rule.
Key takeaway
Tests are the first client of your code. Clean, fast, focused tests make fearless refactoring possible.