Building fizzbuzz test-first
The spec: fizzbuzz(n) returns "Fizz" for multiples of 3, "Buzz" for multiples of 5, "FizzBuzz" for multiples of both, and the number as a string otherwise.
Beat 1, red. We write two tests before any real code. The function is a stub returning None, so both tests must fail. Run the next block and enjoy the red: this failure is planned and it proves both tests can detect a broken implementation.
Beat 1, the planned failure
Two tests written before any real code. The function is a stub returning None, so both must fail, and this failing report is the intended result of the step.
def fizzbuzz(n): return None def test_multiples_of_3_say_fizz(): assert fizzbuzz(3) == "Fizz" def test_other_numbers_echo_as_text(): assert fizzbuzz(4) == "4" tests = [test_multiples_of_3_say_fizz, test_other_numbers_echo_as_text] passed = 0 for test in tests: try: test() print("PASS", test.__name__) passed += 1 except AssertionError: print("FAIL", test.__name__) print(f"{passed}/{len(tests)} passed")
Output
FAIL test_multiples_of_3_say_fizz FAIL test_other_numbers_echo_as_text 0/2 passed
Both assertions compare against an exact string, which is what makes them fail against None. Compare that with the assert not result from the previous lesson, which would have passed against this same stub and taught you nothing.
The 0/2 passed line is the useful artifact of this step. It confirms that both tests can detect a broken implementation, so when they turn green later, the green means something.
Note that the tests are already checking two distinct behaviors, the Fizz rule and the echo rule. Strict TDD would add them one at a time, and doing two at once is a reasonable compromise when the behaviors are this small and independent.
Beat 2, green. Write just enough code to pass, run, and the report flips. Beat 3, refactor if anything is ugly, keeping the report green.
Then the loop repeats: next red test ("Buzz" for 5), minimal green, refactor. Then again for "FizzBuzz" at 15. The next block shows the state after three full loops, with all four behaviors tested and implemented. Notice the order of the if checks: 15 must be tested first, because a multiple of 15 is also a multiple of 3, and an early "Fizz" return would win. That subtlety was forced out by the red test for 15, not remembered by luck.
After three loops, all green
The state after three red-green-refactor cycles, with four behaviors tested and implemented.
def fizzbuzz(n): if n % 15 == 0: return "FizzBuzz" if n % 3 == 0: return "Fizz" if n % 5 == 0: return "Buzz" return str(n) def test_multiples_of_3_say_fizz(): assert fizzbuzz(3) == "Fizz" def test_multiples_of_5_say_buzz(): assert fizzbuzz(5) == "Buzz" def test_multiples_of_both_say_fizzbuzz(): assert fizzbuzz(15) == "FizzBuzz" def test_other_numbers_echo_as_text(): assert fizzbuzz(4) == "4" tests = [test_multiples_of_3_say_fizz, test_multiples_of_5_say_buzz, test_multiples_of_both_say_fizzbuzz, test_other_numbers_echo_as_text] passed = 0 for test in tests: try: test() print("PASS", test.__name__) passed += 1 except AssertionError: print("FAIL", test.__name__) print(f"{passed}/{len(tests)} passed")
Output
PASS test_multiples_of_3_say_fizz PASS test_multiples_of_5_say_buzz PASS test_multiples_of_both_say_fizzbuzz PASS test_other_numbers_echo_as_text 4/4 passed
The order of the if checks is the substance of this example. The 15 case must be tested first, because a multiple of 15 is also a multiple of 3, so an earlier "Fizz" return would win and 15 would come back as "Fizz".
That ordering was forced out by the red test for 15 rather than remembered by luck, which is the clearest single demonstration of what TDD buys you. Writing the implementation first, most people write the 3 and 5 branches in the obvious order and discover the 15 problem later, or never.
return str(n) for the fallthrough case is why the echo test asserts the string "4" and not the integer 4. Returning a mix of strings and numbers from one function is a real design smell, and the test locks in the consistent choice.
Playing the green beat
The red tests for is_palindrome are already written and failing. The task is the green step: make all three pass.
def is_palindrome(text): lowered = text.lower() return lowered == lowered[::-1] def test_palindrome_word(): assert is_palindrome("level") == True def test_non_palindrome_word(): assert is_palindrome("python") == False def test_ignores_case(): assert is_palindrome("Level") == True tests = [test_palindrome_word, test_non_palindrome_word, test_ignores_case] passed = 0 for test in tests: try: test() print("PASS", test.__name__) passed += 1 except AssertionError: print("FAIL", test.__name__) print(f"{passed}/{len(tests)} passed")
Output
PASS test_palindrome_word PASS test_non_palindrome_word PASS test_ignores_case 3/3 passed
Slicing with [::-1] reverses a string, which is the trick from Advanced Python, and comparing a string to its reverse is the whole definition of a palindrome.
The lowering has to happen before the comparison, and doing it once into lowered is what makes both sides agree. Writing text.lower() == text[::-1] would compare a lowercased string against an unmodified reverse, and test_ignores_case is the test that catches it.
Note which behavior the third test forced into existence. Without it, return text == text[::-1] passes the first two tests, so the case-insensitivity is in the code only because a test demanded it. That is TDD working exactly as intended.
A real palindrome checker would also need to ignore spaces and punctuation, so that a phrase like "A man, a plan, a canal: Panama" counts. There is no test for that here, so the code correctly does not implement it, which is the discipline of writing no more than the tests require.