Week 12 Tutorial: Unit Testing and Test-Driven Development
Problem 1 (Easy): Pizza Slice Counter Tests
A small pizzeria has a helper function that divides a pizza fairly. Slices are split equally among people using integer division. The kitchen manager wants automated tests so the function never breaks silently.
You are given the file pizza.py with the following function:
def slices_per_person(total_slices, people):
return total_slices // people
- Create a test file named
test_pizza.pyin the same folder. - Import the
slices_per_personfunction frompizza. - Write a test function
test_even_splitthat asserts 8 slices among 4 people gives 2. - Write a test function
test_uneven_splitthat asserts 10 slices among 3 people gives 3. - Write a test function
test_one_eachthat asserts 5 slices among 5 people gives 1. - Run
pytestfrom the terminal and confirm all three tests pass.
Problem 2 (Easy+): Palindrome Checker Tests
A word game app needs a reliable palindrome checker. A palindrome is a string that reads the same forwards and backwards (case-insensitive). The team already has the function — your job is to lock its behavior with tests so future changes can’t break it.
You are given the file palindrome.py:
def is_palindrome(text):
cleaned = text.lower()
return cleaned == cleaned[::-1]
- Create a test file named
test_palindrome.py. - Import
is_palindromefrompalindrome. - Write
test_simple_palindromeasserting that"level"is a palindrome (returnsTrue). - Write
test_mixed_case_palindromeasserting that"RaceCar"is a palindrome. - Write
test_not_a_palindromeasserting that"python"is NOT a palindrome (useassert not ...). - Write
test_single_characterasserting that"a"is a palindrome. - Write
test_empty_stringasserting that""is a palindrome. - Run
pytestand confirm all five tests pass.
Problem 3 (Medium): Loyalty Card Tests
A coffee chain has a LoyaltyCard class that tracks reward points for each customer. The class uses a property to expose points as read-only and raises ValueError for invalid operations. Your job is to write a thorough test suite for it.
You are given the file loyalty.py:
class LoyaltyCard:
def __init__(self, owner: str):
self.owner = owner
self._points = 0
@property
def points(self) -> int:
return self._points
def earn(self, amount: int) -> None:
if amount <= 0:
raise ValueError("Earned amount must be positive")
self._points += amount
def redeem(self, amount: int) -> None:
if amount <= 0:
raise ValueError("Redeem amount must be positive")
if amount > self._points:
raise ValueError("Not enough points")
self._points -= amount
- Create
test_loyalty.pyand importLoyaltyCard. - Write tests that cover the happy paths of
earnandredeem, including the initial state of a new card. - Write tests that cover the error paths — invalid amounts and overspending. Use
pytest.raisesto verify eachValueError. - Make sure the
pointsproperty cannot be modified directly from outside (write a test that proves this). - Aim for at least 6 test functions, each focused on one behavior. Choose your own scenarios and test names.
- All tests must pass when you run
pytest.
Problem 4 (Medium+): Ticket Booth Tests
A concert venue uses a TicketBooth class to manage ticket sales. The booth tracks remaining seats, exposes a sold-out flag, supports len() (which returns how many tickets have been sold), and raises ValueError for invalid sales. Build a clean test suite that uses a fixture and parametrize so the suite is not repetitive.
You are given the file ticket_booth.py:
class TicketBooth:
def __init__(self, event: str, capacity: int):
self.event = event
self._capacity = capacity
self._sold = 0
@property
def remaining(self) -> int:
return self._capacity - self._sold
@property
def is_sold_out(self) -> bool:
return self._sold >= self._capacity
def sell(self, quantity: int) -> None:
if quantity <= 0:
raise ValueError("Quantity must be positive")
if quantity > self.remaining:
raise ValueError("Not enough tickets")
self._sold += quantity
def __len__(self) -> int:
return self._sold
- Create
test_ticket_booth.pyand import what you need. - Define a
pytest.fixturenamedbooththat returns a freshTicketBooth("Rock Night", 100)for each test. - Verify the initial state of a new booth (remaining seats,
len(), sold-out flag). - Use
@pytest.mark.parametrizeto test that selling several different valid quantities updatesremainingandlen(booth)correctly. - Cover the error paths: non-positive quantity and selling more than the remaining capacity. Use
pytest.raises. - Verify that
is_sold_outbecomesTrueafter the booth fills up exactly, and that further sales fail.
Problem 5 (Advanced): Weather Reporter — Find the Bugs with Mocked Tests
A travel app uses a WeatherReporter that calls a third-party HTTP API. The previous developer left buggy code behind and no tests. Your job: read the requirements below (they are the source of truth, NOT the code), write a test suite that locks the intended behavior, run it (it will fail), and then fix the source code until every test passes. You cannot hit the real API — use unittest.mock to replace requests.get.
You are given the file weather_reporter.py (broken — do not trust it):
import requests
class WeatherReporter:
def __init__(self, api_url: str):
self.api_url = api_url
def get_report(self, city: str) -> str:
response = requests.get(f"{self.api_url}/forecast", params={"location": city})
if response.status_code == 200:
raise RuntimeError(f"Failed to fetch weather for {city}")
data = response.json()
return f"{city}: {data['condition']}, {data['temp']}°C"
- Create
test_weather_reporter.py. Importpatch,MagicMock,pytest, and the class under test. - Patch
requests.getinside theweather_reportermodule so no real HTTP request is made. - Write a happy-path test that:
- Builds a
MagicMockresponse whose.status_codeis200and whose.json()returns a dict with"temp"and"condition"keys. - Assigns that response as the patched
requests.getreturn value. - Calls
get_report(city)and asserts it returns the stringf"{city}: {data['temp']}°C, {data['condition']}". - Asserts the patched
requests.getwas called exactly once with URLf"{api_url}/weather"and query params{"city": city}(useassert_called_once_withor inspectcall_args).
- Builds a
- Use
@pytest.mark.parametrizeto repeat the happy path for several different cities/payloads in one test function. - Write a test that proves any
status_code != 200triggersRuntimeErrorwhose message isf"Failed to fetch weather for {city}".
Problem 6 (Advanced+): Subscription Service — Find the Bugs with Mocked Tests
A streaming platform charges users on a recurring schedule. A junior developer wrote the billing module but it has several subtle bugs and no tests. Your job: read the requirements below (they are the source of truth, NOT the code), build a test suite that locks the intended behavior, run it (it will fail), then fix subscriptions.py until every test passes. The service must never call a real email client — use unittest.mock.
You are given the file subscriptions.py (broken — do not trust it):
from abc import ABC, abstractmethod
from datetime import date, timedelta
class Subscription(ABC):
PRICE: int
def __init__(self, user_email: str, start_date: date):
self.user_email = user_email
self.start_date = start_date
self.renewals = 0
@abstractmethod
def next_billing_date(self) -> date: ...
def is_due(self, today: date) -> bool:
return today > self.next_billing_date()
def renew(self) -> None:
self.renewals = 1
class MonthlySubscription(Subscription):
PRICE = 10
def next_billing_date(self) -> date:
return self.start_date + timedelta(days=30 * self.renewals)
class AnnualSubscription(Subscription):
PRICE = 100
def next_billing_date(self) -> date:
return self.start_date + timedelta(days=365 * (self.renewals + 1))
class SubscriptionService:
def __init__(self, email_client):
self.email_client = email_client
self.subscriptions: list[Subscription] = []
def add(self, subscription: Subscription) -> None:
self.subscriptions.append(subscription)
def renew_due(self, today: date) -> int:
renewed = 0
for sub in self.subscriptions:
if sub.is_due(today):
self.email_client.send(
sub.user_email,
f"Renewed: ${sub.PRICE}",
)
renewed += 1
return renewed
- Create
test_subscriptions.py. Useunittest.mockto fake the email client — the service must never call a real one. - Define a
pytest.fixturenamedservicethat returnsSubscriptionService(MagicMock()). Inside tests you can access the mock email client viaservice.email_clientand assert calls on it (e.g.service.email_client.send.assert_called_once_with(...)). - Prove that
Subscriptionis abstract — instantiating it directly must raiseTypeError. A new concrete subscription (i.e.MonthlySubscriptionorAnnualSubscription) must exposeuser_email,start_date, andrenewals == 0. - Use
@pytest.mark.parametrizeto verifynext_billing_datefor several renewal counts (e.g.,0, 1, 2, 5):MonthlySubscription(PRICE = 10) returnsstart_date + timedelta(days=30 * (renewals + 1))— i.e. a fresh monthly sub is first due 30 days afterstart_date, then 60, then 90, …AnnualSubscription(PRICE = 100) returnsstart_date + timedelta(days=365 * (renewals + 1)).
- Write a test that calls
renew()multiple times on a single subscription and assertsrenewalsincreases by exactly1each call —renew()must increment, not reset (catches the “reset to 1” bug). - Write a test for
is_due(today)asserting it returnsTruewhentoday == next_billing_date()and whentoday > next_billing_date(), andFalsewhentoday < next_billing_date()— being due on the billing date counts (catches the strict>bug). - Verify that
renew_due(today)iterates all subscriptions and, for every one that is due: callssub.renew(), callsemail_client.send(sub.user_email, f"Renewed: ${sub.PRICE}")exactly once, and counts it; subscriptions that are not due must be skipped. The return value equals the total number renewed. - Verify that running
renew_duetwice on the same day does not double-charge — once a subscription is renewed its next billing date moves into the future. Use the mock’scall_countto confirm exactly one email per subscription (catches the “forgot to callsub.renew()” bug). - Cover a “no one is due” scenario and assert the email client was not called (
assert_not_called). - Run
pytest— your tests should fail against the buggy code. Now fix the four bugs (is_duecomparison,renewassignment,MonthlySubscription.next_billing_dateformula, missingsub.renew()call inrenew_due) until every test passes.
Problem 7 (TDD): Habit Tracker via TDD
A productivity app needs a HabitTracker class. You will build it using strict Test-Driven Development: write a failing test first (RED), implement the minimum code to make it pass (GREEN), refactor if needed, then move to the next test. The class is not given to you — you design it from the specification below.
Specification for HabitTracker (in habit_tracker.py):
- Constructor and
total
A new tracker is created with aname(e.g.HabitTracker("Read 30 min")). It exposes a read-only propertytotalthat returns the count of distinct completed days. New trackers start at0.
Write a failing test that asserts the tracker’s name and thattotal == 0, then implement the constructor and property to make it pass (Red → Green). Create apytest.fixturethat returns a freshHabitTracker("Read 30 min")so you can reuse it in the remaining tests. mark_done(day: date)— single and multiple days
This method records completion for the givendate.
Write a test that marks a single day and assertstotalbecomes1; implement just enough code to pass. Then write a test that marks several distinct days and assertstotalequals the number of unique days added.mark_done(day: date)— duplicate error
Callingmark_donewith a date that is already marked must raiseValueError.
Usepytest.raisesto write a test that marks a day, tries to mark it again, and asserts the exception is raised whiletotalstays unchanged.__contains__(day)
Supportsday in trackersyntax; returns whether the date has been marked.
Test this by marking one day, then asserting the marked date isinthe tracker and an unmarked date isnot init.streak(today: date)
Returns the length of the consecutive run of completions ending ontoday. Iftodayitself is not marked, the streak is0. If yesterday is marked but today is not, the streak is still0(the run must end ontoday).
Use@pytest.mark.parametrizeto cover several scenarios in one test function: a streak of 1, a multi-day streak, a broken streak (gap in the middle), and a “today not marked” case. For each scenario, mark the required dates, callstreak(today), and assert the expected length.