ooad1-3 Exercise 3: 'Return a Book' as Tests =============================================== ex3.py (Python 3.14; each step and extension of the use case is a comment where it happens): import unittest from dataclasses import dataclass, field from datetime import date, timedelta FINE_PENCE_PER_DAY = 10 # assumption from the use case: ask the librarian class Refusal(Exception): pass @dataclass class Loan: card: str isbn: str due_on: date returned_on: date | None = None @dataclass class Copy: isbn: str status: str = "on_loan" # on_loan | available | held | withdrawn held_for: str | None = None @dataclass class Library: loans: list = field(default_factory=list) copies: dict = field(default_factory=dict) fines: dict = field(default_factory=dict) # card -> pence queue: dict = field(default_factory=dict) # isbn -> [cards waiting] def return_book(self, isbn: str, today: date, damaged: bool = False) -> int: # 3. The system finds the open loan for that copy. loan = next((l for l in self.loans if l.isbn == isbn and l.returned_on is None), None) if loan is None: # 3a. raise Refusal("that copy is not on loan") # 4. The system checks the book is back by its due date. fine = 0 if today > loan.due_on: # 4a. fine = (today - loan.due_on).days * FINE_PENCE_PER_DAY self.fines[loan.card] = self.fines.get(loan.card, 0) + fine # 5. The system closes the loan and marks the copy available. loan.returned_on = today copy = self.copies[isbn] waiting = self.queue.get(isbn, []) if damaged: # 5b. copy.status = "withdrawn" elif waiting: # 5a. copy.status, copy.held_for = "held", waiting.pop(0) else: copy.status = "available" # 6. The system confirms to the Librarian (here: returns the fine to mention). return fine def library(**kw): lib = Library(copies={"111": Copy("111")}, **kw) lib.loans.append(Loan("M1", "111", due_on=date(2026, 10, 10))) return lib class ReturnBook(unittest.TestCase): def test_main_flow(self): lib = library() self.assertEqual(lib.return_book("111", date(2026, 10, 9)), 0) self.assertEqual(lib.copies["111"].status, "available") self.assertEqual(lib.fines, {}) def test_3a_not_on_loan(self): lib = library() lib.return_book("111", date(2026, 10, 9)) with self.assertRaises(Refusal): lib.return_book("111", date(2026, 10, 9)) def test_due_today_is_not_overdue(self): self.assertEqual(library().return_book("111", date(2026, 10, 10)), 0) def test_4a_overdue_fine(self): lib = library() self.assertEqual(lib.return_book("111", date(2026, 10, 13)), 30) self.assertEqual(lib.fines, {"M1": 30}) def test_5a_next_in_queue_gets_a_hold(self): lib = library(queue={"111": ["M2", "M3"]}) lib.return_book("111", date(2026, 10, 9)) self.assertEqual((lib.copies["111"].status, lib.copies["111"].held_for), ("held", "M2")) self.assertEqual(lib.queue["111"], ["M3"]) def test_5b_damaged_is_withdrawn(self): lib = library() lib.return_book("111", date(2026, 10, 9), damaged=True) self.assertEqual(lib.copies["111"].status, "withdrawn") def test_open_question_damaged_copy_leaves_the_reservation_waiting(self): lib = library(queue={"111": ["M2"]}) lib.return_book("111", date(2026, 10, 9), damaged=True) self.assertEqual(lib.copies["111"].status, "withdrawn") self.assertEqual(lib.queue["111"], ["M2"]) # M2 is still waiting, but for a copy that no longer exists if __name__ == "__main__": unittest.main(verbosity=2) Run with: py ex3.py Output: test_3a_not_on_loan (__main__.ReturnBook.test_3a_not_on_loan) ... ok test_4a_overdue_fine (__main__.ReturnBook.test_4a_overdue_fine) ... ok test_5a_next_in_queue_gets_a_hold (__main__.ReturnBook.test_5a_next_in_queue_gets_a_hold) ... ok test_5b_damaged_is_withdrawn (__main__.ReturnBook.test_5b_damaged_is_withdrawn) ... ok test_due_today_is_not_overdue (__main__.ReturnBook.test_due_today_is_not_overdue) ... ok test_main_flow (__main__.ReturnBook.test_main_flow) ... ok test_open_question_damaged_copy_leaves_the_reservation_waiting (__main__.ReturnBook.test_open_question_damaged_copy_leaves_the_reservation_waiting) ... ok ---------------------------------------------------------------------- Ran 7 tests in 0.001s OK A case the description did not decide: the last test. When a damaged book is returned while another Member is waiting in the queue, the code withdraws the copy (5b wins over 5a), but the waiting Member stays in the queue for a copy that no longer exists. The test passes because it records what the code does, not because it is the right answer. Questions for the librarian: Should the reservation move to another copy of the same title, if one exists? Should the Member be told? Should a damaged return that is not the Member's fault (the book was already damaged) still be fined? WHY THIS WORKS AS AN ANSWER --------------------------- One test per flow shows that every extension is reachable and ends as described. The boundary test (due today is not overdue) comes from asking what "by its due date" means, a question the description did not answer either. Naming the last test as an open question, rather than hiding it, keeps the code honest about the gap until the rule is decided.