← Computer Programming II

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
  1. Create a test file named test_pizza.py in the same folder.
  2. Import the slices_per_person function from pizza.
  3. Write a test function test_even_split that asserts 8 slices among 4 people gives 2.
  4. Write a test function test_uneven_split that asserts 10 slices among 3 people gives 3.
  5. Write a test function test_one_each that asserts 5 slices among 5 people gives 1.
  6. Run pytest from 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]
  1. Create a test file named test_palindrome.py.
  2. Import is_palindrome from palindrome.
  3. Write test_simple_palindrome asserting that "level" is a palindrome (returns True).
  4. Write test_mixed_case_palindrome asserting that "RaceCar" is a palindrome.
  5. Write test_not_a_palindrome asserting that "python" is NOT a palindrome (use assert not ...).
  6. Write test_single_character asserting that "a" is a palindrome.
  7. Write test_empty_string asserting that "" is a palindrome.
  8. Run pytest and 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
  1. Create test_loyalty.py and import LoyaltyCard.
  2. Write tests that cover the happy paths of earn and redeem, including the initial state of a new card.
  3. Write tests that cover the error paths — invalid amounts and overspending. Use pytest.raises to verify each ValueError.
  4. Make sure the points property cannot be modified directly from outside (write a test that proves this).
  5. Aim for at least 6 test functions, each focused on one behavior. Choose your own scenarios and test names.
  6. 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
  1. Create test_ticket_booth.py and import what you need.
  2. Define a pytest.fixture named booth that returns a fresh TicketBooth("Rock Night", 100) for each test.
  3. Verify the initial state of a new booth (remaining seats, len(), sold-out flag).
  4. Use @pytest.mark.parametrize to test that selling several different valid quantities updates remaining and len(booth) correctly.
  5. Cover the error paths: non-positive quantity and selling more than the remaining capacity. Use pytest.raises.
  6. Verify that is_sold_out becomes True after 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"
  1. Create test_weather_reporter.py. Import patch, MagicMock, pytest, and the class under test.
  2. Patch requests.get inside the weather_reporter module so no real HTTP request is made.
  3. Write a happy-path test that:
    • Builds a MagicMock response whose .status_code is 200 and whose .json() returns a dict with "temp" and "condition" keys.
    • Assigns that response as the patched requests.get return value.
    • Calls get_report(city) and asserts it returns the string f"{city}: {data['temp']}°C, {data['condition']}".
    • Asserts the patched requests.get was called exactly once with URL f"{api_url}/weather" and query params {"city": city} (use assert_called_once_with or inspect call_args).
  4. Use @pytest.mark.parametrize to repeat the happy path for several different cities/payloads in one test function.
  5. Write a test that proves any status_code != 200 triggers RuntimeError whose message is f"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
  1. Create test_subscriptions.py. Use unittest.mock to fake the email client — the service must never call a real one.
  2. Define a pytest.fixture named service that returns SubscriptionService(MagicMock()). Inside tests you can access the mock email client via service.email_client and assert calls on it (e.g. service.email_client.send.assert_called_once_with(...)).
  3. Prove that Subscription is abstract — instantiating it directly must raise TypeError. A new concrete subscription (i.e. MonthlySubscription or AnnualSubscription) must expose user_email, start_date, and renewals == 0.
  4. Use @pytest.mark.parametrize to verify next_billing_date for several renewal counts (e.g., 0, 1, 2, 5):
    • MonthlySubscription (PRICE = 10) returns start_date + timedelta(days=30 * (renewals + 1)) — i.e. a fresh monthly sub is first due 30 days after start_date, then 60, then 90, …
    • AnnualSubscription (PRICE = 100) returns start_date + timedelta(days=365 * (renewals + 1)).
  5. Write a test that calls renew() multiple times on a single subscription and asserts renewals increases by exactly 1 each call — renew() must increment, not reset (catches the “reset to 1” bug).
  6. Write a test for is_due(today) asserting it returns True when today == next_billing_date() and when today > next_billing_date(), and False when today < next_billing_date() — being due on the billing date counts (catches the strict > bug).
  7. Verify that renew_due(today) iterates all subscriptions and, for every one that is due: calls sub.renew(), calls email_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.
  8. Verify that running renew_due twice on the same day does not double-charge — once a subscription is renewed its next billing date moves into the future. Use the mock’s call_count to confirm exactly one email per subscription (catches the “forgot to call sub.renew()” bug).
  9. Cover a “no one is due” scenario and assert the email client was not called (assert_not_called).
  10. Run pytest — your tests should fail against the buggy code. Now fix the four bugs (is_due comparison, renew assignment, MonthlySubscription.next_billing_date formula, missing sub.renew() call in renew_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):

  1. Constructor and total
    A new tracker is created with a name (e.g. HabitTracker("Read 30 min")). It exposes a read-only property total that returns the count of distinct completed days. New trackers start at 0.
    Write a failing test that asserts the tracker’s name and that total == 0, then implement the constructor and property to make it pass (Red → Green). Create a pytest.fixture that returns a fresh HabitTracker("Read 30 min") so you can reuse it in the remaining tests.
  2. mark_done(day: date) — single and multiple days
    This method records completion for the given date.
    Write a test that marks a single day and asserts total becomes 1; implement just enough code to pass. Then write a test that marks several distinct days and asserts total equals the number of unique days added.
  3. mark_done(day: date) — duplicate error
    Calling mark_done with a date that is already marked must raise ValueError.
    Use pytest.raises to write a test that marks a day, tries to mark it again, and asserts the exception is raised while total stays unchanged.
  4. __contains__(day)
    Supports day in tracker syntax; returns whether the date has been marked.
    Test this by marking one day, then asserting the marked date is in the tracker and an unmarked date is not in it.
  5. streak(today: date)
    Returns the length of the consecutive run of completions ending on today. If today itself is not marked, the streak is 0. If yesterday is marked but today is not, the streak is still 0 (the run must end on today).
    Use @pytest.mark.parametrize to 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, call streak(today), and assert the expected length.

📁 Tutorial Project: BudgetWise