Add the Menu, MenuItem, CoffeeMaker and MoneyMachine classes with the assignment's method names, and a main loop that supports drink names, report and off. Resources are checked before payment, and payment before the drink is made. Move drink prices, ingredient amounts and all message text into constants.py. Add 25 pytest tests for every class and the main loop, and record the review against QC-PY-001 as RC-007. Task: MIL-002#1 Task: MIL-002#2 Task: MIL-002#3 Task: MIL-002#4 Task: MIL-002#5 Task: MIL-002#6 Closes #6 Closes #7 Closes #8 Closes #9 Closes #10 Closes #11
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
"""! @file
|
||||
@brief Shared test helpers.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
import pytest
|
||||
|
||||
InputFeeder = Callable[[Iterable[str]], None]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def feed_input(monkeypatch: pytest.MonkeyPatch) -> InputFeeder:
|
||||
"""! @brief Return a function that makes input() answer from a list."""
|
||||
|
||||
def feed(answers: Iterable[str]) -> None:
|
||||
iterator = iter(answers)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": next(iterator))
|
||||
|
||||
return feed
|
||||
@@ -0,0 +1,68 @@
|
||||
"""! @file
|
||||
@brief Tests for CoffeeMaker.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from coffee_machine import constants
|
||||
from coffee_machine.coffee_maker import CoffeeMaker
|
||||
from coffee_machine.menu import Menu, MenuItem
|
||||
|
||||
|
||||
def _drink(name: str) -> MenuItem:
|
||||
drink = Menu().find_drink(name)
|
||||
assert drink is not None
|
||||
return drink
|
||||
|
||||
|
||||
def test_report_prints_each_resource_with_unit(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
CoffeeMaker().report()
|
||||
|
||||
assert capsys.readouterr().out.splitlines() == [
|
||||
"Water: 300ml",
|
||||
"Milk: 200ml",
|
||||
"Coffee: 100g",
|
||||
]
|
||||
|
||||
|
||||
def test_is_resource_sufficient_when_enough() -> None:
|
||||
assert CoffeeMaker().is_resource_sufficient(_drink("latte"))
|
||||
|
||||
|
||||
def test_is_resource_sufficient_when_exactly_enough() -> None:
|
||||
maker = CoffeeMaker()
|
||||
maker.resources.update(
|
||||
{constants.WATER: 50, constants.MILK: 0, constants.COFFEE: 18}
|
||||
)
|
||||
|
||||
assert maker.is_resource_sufficient(_drink("espresso"))
|
||||
|
||||
|
||||
def test_is_resource_sufficient_when_short_names_each_missing_ingredient(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
maker = CoffeeMaker()
|
||||
maker.resources.update({constants.WATER: 10, constants.MILK: 10})
|
||||
|
||||
assert not maker.is_resource_sufficient(_drink("latte"))
|
||||
out = capsys.readouterr().out
|
||||
assert "not enough water" in out
|
||||
assert "not enough milk" in out
|
||||
assert "not enough coffee" not in out
|
||||
|
||||
|
||||
def test_make_coffee_deducts_ingredients_and_serves(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
maker = CoffeeMaker()
|
||||
|
||||
maker.make_coffee(_drink("latte"))
|
||||
|
||||
assert maker.resources == {
|
||||
constants.WATER: 100,
|
||||
constants.MILK: 50,
|
||||
constants.COFFEE: 76,
|
||||
}
|
||||
assert "latte" in capsys.readouterr().out
|
||||
@@ -5,10 +5,14 @@
|
||||
from coffee_machine import constants
|
||||
|
||||
|
||||
def test_every_drink_has_a_cost_and_ingredients() -> None:
|
||||
assert set(constants.DRINK_COSTS) == set(constants.DRINK_INGREDIENTS)
|
||||
|
||||
|
||||
def test_every_drink_uses_known_ingredients() -> None:
|
||||
known = set(constants.INITIAL_RESOURCES)
|
||||
for drink in constants.MENU.values():
|
||||
assert set(drink["ingredients"]) <= known # type: ignore[call-overload]
|
||||
for ingredients in constants.DRINK_INGREDIENTS.values():
|
||||
assert set(ingredients) <= known
|
||||
|
||||
|
||||
def test_every_resource_has_a_unit() -> None:
|
||||
@@ -16,5 +20,5 @@ def test_every_resource_has_a_unit() -> None:
|
||||
|
||||
|
||||
def test_commands_are_not_drink_names() -> None:
|
||||
assert constants.COMMAND_OFF not in constants.MENU
|
||||
assert constants.COMMAND_REPORT not in constants.MENU
|
||||
assert constants.COMMAND_OFF not in constants.DRINK_COSTS
|
||||
assert constants.COMMAND_REPORT not in constants.DRINK_COSTS
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""! @file
|
||||
@brief Tests for the main loop.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from coffee_machine.main import main
|
||||
from tests.conftest import InputFeeder
|
||||
|
||||
PAY_150 = ["6", "0", "0", "0"]
|
||||
PAY_250 = ["10", "0", "0", "0"]
|
||||
PAY_NOTHING = ["0", "0", "0", "0"]
|
||||
|
||||
|
||||
def test_off_stops_the_loop(feed_input: InputFeeder) -> None:
|
||||
feed_input(["off"])
|
||||
|
||||
main()
|
||||
|
||||
|
||||
def test_report_shows_resources_and_money(
|
||||
feed_input: InputFeeder, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed_input(["report", "off"])
|
||||
|
||||
main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Water: 300ml" in out
|
||||
assert "Money: $0.00" in out
|
||||
|
||||
|
||||
def test_successful_order_makes_drink_and_updates_report(
|
||||
feed_input: InputFeeder, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed_input(["espresso", *PAY_150, "report", "off"])
|
||||
|
||||
main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Here is your espresso" in out
|
||||
assert "Water: 250ml" in out
|
||||
assert "Money: $1.50" in out
|
||||
|
||||
|
||||
def test_choice_is_case_insensitive(
|
||||
feed_input: InputFeeder, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed_input([" Espresso ", *PAY_150, "off"])
|
||||
|
||||
main()
|
||||
|
||||
assert "Here is your espresso" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_unknown_drink_asks_again_without_payment(
|
||||
feed_input: InputFeeder, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed_input(["tea", "off"])
|
||||
|
||||
main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "not available" in out
|
||||
assert "insert coins" not in out
|
||||
|
||||
|
||||
def test_insufficient_resources_stop_before_payment(
|
||||
feed_input: InputFeeder, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
# The second latte needs 150 ml milk but only 50 ml is left.
|
||||
feed_input(["latte", *PAY_250, "latte", "off"])
|
||||
|
||||
main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert out.count("insert coins") == 1
|
||||
assert "not enough milk" in out
|
||||
|
||||
|
||||
def test_short_payment_is_refunded_and_no_drink_is_made(
|
||||
feed_input: InputFeeder, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed_input(["espresso", *PAY_NOTHING, "report", "off"])
|
||||
|
||||
main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Money refunded" in out
|
||||
assert "Enjoy" not in out
|
||||
assert "Water: 300ml" in out
|
||||
@@ -0,0 +1,28 @@
|
||||
"""! @file
|
||||
@brief Tests for Menu and MenuItem.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from coffee_machine import constants
|
||||
from coffee_machine.menu import Menu
|
||||
|
||||
|
||||
def test_get_items_lists_all_drinks_separated() -> None:
|
||||
assert Menu().get_items() == "espresso/latte/cappuccino"
|
||||
|
||||
|
||||
def test_find_drink_returns_matching_item() -> None:
|
||||
drink = Menu().find_drink("latte")
|
||||
|
||||
assert drink is not None
|
||||
assert drink.name == "latte"
|
||||
assert drink.cost == constants.DRINK_COSTS["latte"]
|
||||
assert drink.ingredients == constants.DRINK_INGREDIENTS["latte"]
|
||||
|
||||
|
||||
def test_find_drink_when_unknown_returns_none_and_prints_message(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
assert Menu().find_drink("tea") is None
|
||||
assert constants.MSG_UNAVAILABLE in capsys.readouterr().out
|
||||
@@ -0,0 +1,62 @@
|
||||
"""! @file
|
||||
@brief Tests for MoneyMachine.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from coffee_machine.money_machine import MoneyMachine
|
||||
from tests.conftest import InputFeeder
|
||||
|
||||
|
||||
def test_report_prints_profit(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
MoneyMachine().report()
|
||||
|
||||
assert capsys.readouterr().out == "Money: $0.00\n"
|
||||
|
||||
|
||||
def test_process_coins_adds_up_coin_values(feed_input: InputFeeder) -> None:
|
||||
feed_input(["1", "2", "3", "4"]) # quarters, dimes, nickels, pennies
|
||||
|
||||
assert MoneyMachine().process_coins() == pytest.approx(0.64)
|
||||
|
||||
|
||||
def test_process_coins_asks_again_on_invalid_count(
|
||||
feed_input: InputFeeder, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed_input(["abc", "-1", "4", "0", "0", "0"])
|
||||
|
||||
assert MoneyMachine().process_coins() == pytest.approx(1.0)
|
||||
assert capsys.readouterr().out.count("whole number") == 2
|
||||
|
||||
|
||||
def test_make_payment_with_exact_amount_keeps_profit(
|
||||
feed_input: InputFeeder, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed_input(["6", "0", "0", "0"]) # $1.50
|
||||
machine = MoneyMachine()
|
||||
|
||||
assert machine.make_payment(1.5)
|
||||
assert machine.profit == pytest.approx(1.5)
|
||||
assert "change" not in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_make_payment_with_excess_gives_change(
|
||||
feed_input: InputFeeder, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed_input(["12", "0", "0", "0"]) # $3.00 for a $2.50 drink
|
||||
machine = MoneyMachine()
|
||||
|
||||
assert machine.make_payment(2.5)
|
||||
assert machine.profit == pytest.approx(2.5)
|
||||
assert "$0.50 in change" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_make_payment_when_short_refunds_and_keeps_no_profit(
|
||||
feed_input: InputFeeder, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
feed_input(["1", "0", "0", "0"])
|
||||
machine = MoneyMachine()
|
||||
|
||||
assert not machine.make_payment(1.5)
|
||||
assert machine.profit == 0.0
|
||||
assert "Money refunded" in capsys.readouterr().out
|
||||
Reference in New Issue
Block a user