The brief
A teammate left the company and left behind wallet, a small module the payments page depends on. Users report odd behavior. Your job across these two lessons: pin the module down with tests, then find and fix every bug.
The agreed spec:
| Function | Contract |
|---|---|
new_wallet() | returns a wallet with balance 0 and empty history |
deposit(w, amount) | amount must be strictly positive, zero or less raises ValueError |
withdraw(w, amount) | strictly positive, may equal the balance exactly, only amounts greater than the balance raise ValueError |
| both operations | append ("deposit", amount) or ("withdraw", amount) to w["history"] |
Today you only write tests. Lesson 4-1 taught why seeing red first matters: each failing test is a discovered bug, documented and reproducible before you change a line.
Five tests, and a red report worth having
Two tests were already green, and three more encode the rest of the spec. The module still has its bugs, so the report below is the intended result.
def new_wallet(): return {"balance": 0, "history": []} def deposit(w, amount): if amount < 0: raise ValueError("amount must be positive") w["balance"] += amount w["history"].append(("deposit", amount)) def withdraw(w, amount): if amount <= 0: raise ValueError("amount must be positive") if amount >= w["balance"]: raise ValueError("insufficient funds") w["balance"] -= amount w["history"].append(("deposit", amount)) def test_deposit_adds_to_balance(): w = new_wallet() deposit(w, 50) assert w["balance"] == 50 def test_deposit_zero_rejected(): w = new_wallet() try: deposit(w, 0) assert False except ValueError: pass def test_withdraw_reduces_balance(): w = new_wallet() deposit(w, 50) withdraw(w, 20) assert w["balance"] == 30 def test_withdraw_exact_balance_allowed(): w = new_wallet() deposit(w, 50) withdraw(w, 50) assert w["balance"] == 0 def test_history_records_withdrawals(): w = new_wallet() deposit(w, 50) withdraw(w, 20) assert w["history"][-1] == ("withdraw", 20) def run_tests(tests): passed = 0 for test in tests: try: test() print("PASS", test.__name__) passed += 1 except AssertionError: print("FAIL", test.__name__) except Exception as e: print(f"FAIL {test.__name__} (crashed: {type(e).__name__})") print(f"{passed}/{len(tests)} passed") run_tests([test_deposit_adds_to_balance, test_deposit_zero_rejected, test_withdraw_reduces_balance, test_withdraw_exact_balance_allowed, test_history_records_withdrawals])
Output
PASS test_deposit_adds_to_balance FAIL test_deposit_zero_rejected PASS test_withdraw_reduces_balance FAIL test_withdraw_exact_balance_allowed (crashed: ValueError) FAIL test_history_records_withdrawals 2/5 passed
test_deposit_zero_rejected uses the expected-exception pattern from lesson 2-2, calling deposit(w, 0) and then assert False inside the try, with except ValueError: pass as the success path. Since the module's guard is amount < 0, zero slips through, no exception is raised, and the assert False fires.
test_withdraw_exact_balance_allowed deposits 50 and withdraws 50, and the module wrongly raises. Note how the runner labels that differently, with (crashed: ValueError), because the generic except Exception caught something that was not an AssertionError. That distinction is free diagnosis: a plain FAIL means an assertion was false, and a crash label means the code blew up before the assertion ran.
test_history_records_withdrawals reads w["history"][-1] and compares against the tuple ("withdraw", 20). It fails because the module records ("deposit", 20), and comparing whole tuples rather than just the amount is what makes the label bug visible at all.
Each test builds its own wallet with new_wallet() in its arrange phase, so no test can be affected by another's balance. That independence is what lets the runner report five separate facts instead of one tangled one.
Freeze the red
You now hold a red report with three named failures, which is a complete bug inventory:
deposit(w, 0)is accepted, spec says reject- withdrawing the exact balance raises
ValueError, spec says allow - withdrawals are recorded in history as
"deposit"
Notice what the tests bought you before any fixing: the bugs are reproducible on demand (lesson 7-2), named (lesson 2-2), and the fix will be provable by a red-to-green flip (lesson 4-3). Carry your five tests into the next lesson unchanged.
The shared signature of two failures
The deposit-zero and withdraw-exact failures share one lesson 3-2 signature: both involve a wrong comparison operator at the exact boundary value.
deposit uses amount < 0 where the spec needs <= 0, and withdraw uses >= where only > should reject. In both cases the boundary value itself, zero for the deposit and the exact balance for the withdrawal, lands on the wrong side of the comparison.
Boundary value analysis predicted exactly these probes, since the technique says to test at the boundary, one below, and one above:
| Function | Boundary | Wrong operator | Spec operator |
|---|---|---|---|
deposit | 0 | amount < 0 | amount <= 0 |
withdraw | exact balance | amount >= balance | amount > balance |
Note that both bugs are invisible to the two tests that were already green, because those tests use 50 and 20, which are comfortably inside the valid range. A suite of typical values would ship this module with confidence.
That is the strongest argument the capstone makes for the checklist habit. The bugs are one character each, they sit at the exact values a spec sentence mentions, and only a test written at those values can find them.