Course outline · 0% complete

0/29 lessons0%

Course overview →

Red, Green, Refactor

lesson 4-1 · ~9 min · 10/29

Probing a lock-out boundary

Lesson 3-2's technique applies directly to a spec that says accounts lock after 5 failed logins. The counts to probe are 4, 5, and 6.

That is boundary value analysis: one step below, exactly at, and one step above. 4 must stay unlocked, 5 must lock, and 6 must stay locked, and the trio catches every mix-up between > and >=.

The middle probe is again the one that does the work, since a wrong > 5 would leave the account unlocked on the fifth attempt while passing both of the outer probes.

Writing the test first

So far you wrote code, then tests. Test-driven development, or TDD, flips the order for two concrete reasons. Tests written after the code tend to be shaped by what the code happens to do rather than what the spec demands, and writing the test first forces you to decide the exact expected behavior before your brain is invested in an implementation.

Plenty of working engineers run this loop daily on core logic, meaning pricing, parsing, and date math. The loop has three beats:

  1. Red: write one small test for behavior that does not exist yet, run it, and watch it fail
  2. Green: write the smallest amount of code that makes the test pass
  3. Refactor: clean the code up while the tests stay green, exactly as in lesson 1-2

Then repeat with the next behavior. Each loop takes a few minutes, which is the property that makes the discipline sustainable.

Why insist on seeing red first? Because a test you have never seen fail is unverified. If it would pass even against broken code, it protects nothing. Watching it go red, and then green after your change, proves the test actually watches the behavior you think it does.

Redfailing testGreenminimal codeRefactorclean up, stay greenone loop takes minutes, then it repeats
The TDD cycle. The gold ring steps through red, green, refactor, then loops.

A test that passes against nothing

A cautionary example. The function is an empty stub, yet the test passes, because assert not result is true when result is None.

def is_valid_username(name):
    return None  # not implemented yet

def test_rejects_empty():
    result = is_valid_username("")
    assert not result

test_rejects_empty()
print("PASS test_rejects_empty (but the function is not even written!)")

Output

PASS test_rejects_empty (but the function is not even written!)

not None is True in Python, so the assert quietly passes against a stub that returns None for every input. This test has never been seen red and is watching nothing at all, which is exactly the failure mode the red beat exists to catch.

The cure is mechanical. Add the accepting case too, as assert is_valid_username("ada"), and that one goes red against the stub. A pair of tests where one fails and one passes proves the pair can actually distinguish working code from broken code.

There is a second lesson buried here about assert not result as a style. It accepts None, False, 0, "", and empty containers alike, so it is a weak claim. Asserting result == False states the expectation precisely and would have failed against the stub immediately.

Why a hard-coded return is acceptable in the green beat

If your test says fizzbuzz(3) == "Fizz", passing it with return "Fizz" is legitimate, because the next red test will force the real logic out of you.

TDD trusts the loop rather than any single step. A hard-coded return passes today's test honestly, and the moment you add fizzbuzz(4) == "4" it goes red, forcing exactly as much real logic as the tests demand and no more.

Two benefits come out of that discipline. The code never gets ahead of what is verified, so there is no untested branch sitting in the file, and every line that exists can be traced to a test that required it.

It also keeps the green beat fast. A minimal change is quick to write and quick to judge, so the cycle stays in minutes rather than stretching into a long session where you lose track of which behavior you were adding.

The habit is only safe because the refactor beat follows. Hard-coded returns and duplicated branches are expected to be ugly, and the passing tests are what make cleaning them up risk-free rather than nerve-racking.