Challenge Goals
3 min- Turn a written spec into a sequence of failing tests.
- Implement minimally, one green at a time.
- Refactor at the end with confidence.
- Finish with ~100% coverage and a clean design.
The Spec · A Shopping Cart
5 minCART SPEC - a new cart is empty (total 0) - add(item, price, qty=1) adds a line - total() = sum of price*qty across lines - adding the same item twice increases its qty - remove(item) deletes the line; removing a missing item raises KeyError - apply_coupon(pct) reduces the total by pct% (0-100) - apply_coupon rejects pct outside 0-100 (ValueError) - a coupon applies to the CURRENT total only
Eight requirements = (at least) eight tests, written one at a time. Resist reading ahead or building the whole thing — the discipline is in the rhythm. Set a timer and go.
The Plan · Map Spec → Tests
14 minTranslate each line to a test name first
spec line test name new cart empty test_new_cart_empty add a line test_add_one_item total sums lines test_total_multiple_items same item increases qty test_add_same_item_stacks remove deletes test_remove_item remove missing raises test_remove_missing_raises coupon reduces total test_coupon_reduces_total coupon out of range raises test_bad_coupon_raises
Then cycle: red → green → refactor, per test
# 🔴 first test import pytest from cart import Cart def test_new_cart_empty(): assert Cart().total() == 0 # 🟢 class Cart: def __init__(self): self._lines = {} def total(self): return 0 # simplest; next test forces real logic
Keep steps tiny
Each cycle should take a minute or two. If you're stuck for long, your step was too big — split the test into something smaller you can pass immediately.
Reference Solution (try first!)
12 minOnly read this after you've attempted the build yourself.
# cart.py class Cart: def __init__(self): self._lines = {} # item -> (price, qty) def add(self, item, price, qty=1): if item in self._lines: p, q = self._lines[item] self._lines[item] = (p, q + qty) else: self._lines[item] = (price, qty) def remove(self, item): del self._lines[item] # raises KeyError if missing — spec ✓ def total(self): return sum(p * q for p, q in self._lines.values()) def apply_coupon(self, pct): if not 0 <= pct <= 100: raise ValueError("pct must be 0-100") return round(self.total() * (1 - pct / 100), 2)
# test_cart.py import pytest from cart import Cart def test_new_cart_empty(): assert Cart().total() == 0 def test_add_one_item(): c = Cart(); c.add("roti", 1.50); assert c.total() == 1.50 def test_total_multiple_items(): c = Cart(); c.add("roti", 1.50, 2); c.add("milo", 3.0) assert c.total() == 6.0 def test_add_same_item_stacks(): c = Cart(); c.add("roti", 1.50); c.add("roti", 1.50) assert c.total() == 3.0 def test_remove_item(): c = Cart(); c.add("roti", 1.50); c.remove("roti") assert c.total() == 0 def test_remove_missing_raises(): with pytest.raises(KeyError): Cart().remove("ghost") def test_coupon_reduces_total(): c = Cart(); c.add("x", 100); assert c.apply_coupon(10) == 90.0 def test_bad_coupon_raises(): with pytest.raises(ValueError): Cart().apply_coupon(150)
$ pytest test_cart.py -v 8 passed $ pytest --cov=cart cart.py ... 100%
Read the diff
Eight tests, eight requirements, 100% coverage — and the Cart class only contains what the tests demanded. If your solution differs but passes the same eight tests, it's equally valid: tests pin behaviour, not implementation. That freedom is TDD working.
Variations to TDD
13 minTDD a WordCounter from a 6-line spec you write yourself (count words, ignore case, top-N, ignore stop-words). Tests first.
Set a 25-minute timer and TDD a Timer or Scoreboard from scratch. How far do you get with strict red-green-refactor?
With a partner, ping-pong TDD: one writes a failing test, the other makes it pass and writes the next failing test. Swap. A classic team practice.
Mini-Challenge · Extend Under Green
8 minTake your green cart and TDD three new requirements: a free-shipping threshold (total > 50 → no shipping fee, else +5), a quantity cap (max 99 per item, else raise), and an itemize() method returning a formatted receipt string. One failing test at a time.
Recap
3 minGiven a spec, translate each requirement into a test name, then cycle red-green-refactor through them one at a time. Tiny steps, constant green. The result: a clean class containing only what tests demanded, with ~100% coverage. Any implementation that passes the tests is valid. Next: integration testing — when units must work together.
Homework
4 minWrite your own 6-8 line spec for a small app (scoreboard, habit tracker, budget splitter), then TDD it from scratch with strict red-green-refactor, committing after each green. Submit the spec, tests, code, and commit log. Target ~100% coverage naturally.
The cart solution is the model. The graded artifact is your commit log proving test-first order, plus ~100% coverage that came for free.