Project Goals
3 min- Add search, filter-by-availability, and sort — test-first.
- Refactor the storage to a real
Bookobject under green tests. - Prove the existing tests catch any regression during refactor.
- End with a clean design and a comprehensive suite.
Warm-Up · Refactor With a Net
5 minPart 1 stored books as {title: available}. To add author, year and genre, we need a richer model. Normally that's scary — but with the Part-1 tests passing, we can rip out the internals and the tests instantly tell us if behaviour changed.
Refactoring = changing how code works without changing what it does. A green test suite is the proof that "what it does" stayed the same. Without tests, every refactor is a gamble; with them, it's routine.
Add Features + Refactor
14 minStep 1 — new tests for richer books (red)
def test_add_with_metadata(cat): cat.add("Dune", author="Herbert", year=1965, genre="scifi") book = cat.get("Dune") assert book.author == "Herbert" assert book.year == 1965
Step 2 — refactor storage to a Book object (green)
from dataclasses import dataclass @dataclass class Book: title: str author: str = "" year: int = 0 genre: str = "" available: bool = True class Catalogue: def __init__(self): self._books = {} # title -> Book def add(self, title, author="", year=0, genre=""): self._books[title] = Book(title, author, year, genre) def get(self, title): self._check(title); return self._books[title] # is_available / borrow / return_book now read book.available
The Part-1 tests (borrow, double-borrow, etc.) must still pass after this internal change — run them constantly while refactoring.
Step 3 — search (test-first)
def test_search_by_author(cat): cat.add("Dune", author="Herbert") cat.add("It", author="King") assert [b.title for b in cat.search(author="Herbert")] == ["Dune"]
def search(self, **criteria): return [b for b in self._books.values() if all(getattr(b, k) == v for k, v in criteria.items())]
Step 4 — filter & sort (test-first)
def test_available_only(cat): cat.add("A"); cat.add("B"); cat.borrow("B") assert [b.title for b in cat.available_books()] == ["A"] def test_sorted_by_year(cat): cat.add("New", year=2020); cat.add("Old", year=1990) assert [b.title for b in cat.sorted_by("year")] == ["Old", "New"]
def available_books(self): return [b for b in self._books.values() if b.available] def sorted_by(self, field): return sorted(self._books.values(), key=lambda b: getattr(b, field))
The Refactor, Verified
12 min# during the Book refactor, run the OLD tests constantly: $ pytest test_catalogue.py -v test_starts_empty PASSED ← still green test_borrow PASSED ← still green test_double_borrow_raises PASSED ← still green ... + new feature tests ... test_add_with_metadata PASSED test_search_by_author PASSED test_available_only PASSED test_sorted_by_year PASSED 14 passed
Suppose mid-refactor you forget to read book.available and a test catches it:
test_borrow FAILED
> assert not cat.is_available("Dune")
E assert not True
# instantly told: is_available still returns the old value. Fix it.Read the diff
This is the whole point. You replaced the storage from a dict-of-bools to a dict-of-Book-objects — a substantial change — and the unchanged Part-1 tests verified that borrow/return/double-borrow behaviour was preserved. When you slipped, a test failed immediately and named the problem. Refactoring under tests is calm and routine; without them it's terrifying.
Try It Yourself
13 minDo the Book-object refactor on your Part-1 catalogue, running the old tests after every change. Did any fail and catch a slip?
TDD search(**criteria) and available_books(). Tests first, then implement.
TDD sorted_by(field, reverse=False). Test ascending and descending by year and by title.
Mini-Challenge · Big Refactor, Zero Behaviour Change
8 minDo a deliberately large refactor: replace the in-memory dict with SQLite-backed storage (from Lesson 24) — WITHOUT changing any test. If all tests stay green, you've proven the behaviour is identical despite a total rewrite of the storage layer.
Recap
3 minYou added features test-first (search, filter, sort) and performed a real refactor (dict → Book objects, even → SQLite) while the existing tests guaranteed behaviour didn't change. That's the deepest payoff of testing: refactoring becomes safe and routine instead of risky. Next: a timed TDD challenge.
Homework
4 minFinish the catalogue: Book-object refactor + search/filter/sort (test-first) + the SQLite-storage refactor with zero test changes. Submit the full suite + code. Note any moment a test caught a regression during refactoring — that's the lesson.
Build on Part-1. The SQLite refactor reuses Lesson 24's in-memory connection. The graded insight: the test suite stayed identical across two major rewrites.