MIL-003: tests, Doxygen check and repository metadata #22
+1
-1
@@ -22,7 +22,7 @@ where = ["src"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
pythonpath = ["src", "."]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""!
|
||||
@file __init__.py
|
||||
@brief Test package.
|
||||
"""
|
||||
@@ -0,0 +1,25 @@
|
||||
"""!
|
||||
@file conftest.py
|
||||
@brief Shared pytest helpers.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
import pytest
|
||||
|
||||
ScriptInput = Callable[[Iterable[str]], None]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def script_input(monkeypatch: pytest.MonkeyPatch) -> ScriptInput:
|
||||
"""!
|
||||
@brief Provide a function that feeds the given answers to `input`.
|
||||
@param monkeypatch pytest's monkeypatch fixture.
|
||||
@return A function taking the answers in the order they are asked.
|
||||
"""
|
||||
|
||||
def feed(answers: Iterable[str]) -> None:
|
||||
remaining = iter(answers)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt: next(remaining))
|
||||
|
||||
return feed
|
||||
@@ -0,0 +1,107 @@
|
||||
"""!
|
||||
@file test_machine.py
|
||||
@brief Tests for making drinks and the main loop.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from coffee_machine.constants import MENU
|
||||
from coffee_machine.main import main, make_coffee
|
||||
from tests.conftest import ScriptInput
|
||||
|
||||
COINS_FOR_LATTE = ["10", "0", "0", "0"]
|
||||
NO_COINS = ["0", "0", "0", "0"]
|
||||
FULL_REPORT = "Water: 300ml\nMilk: 200ml\nCoffee: 100g\nMoney: $0.00"
|
||||
|
||||
|
||||
def test_make_coffee_uses_up_ingredients(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""!
|
||||
@brief The drink's ingredients are deducted and the drink is served.
|
||||
"""
|
||||
resources = {"water": 300, "milk": 200, "coffee": 100}
|
||||
make_coffee("latte", MENU["latte"]["ingredients"], resources)
|
||||
assert resources == {"water": 100, "milk": 50, "coffee": 76}
|
||||
assert capsys.readouterr().out == "Here is your latte. Enjoy!\n"
|
||||
|
||||
|
||||
def test_off_ends_the_machine(script_input: ScriptInput) -> None:
|
||||
"""!
|
||||
@brief Typing off returns from the loop.
|
||||
"""
|
||||
script_input(["off"])
|
||||
main()
|
||||
|
||||
|
||||
def test_report_shows_starting_resources(
|
||||
script_input: ScriptInput, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""!
|
||||
@brief A fresh machine reports full resources and no money.
|
||||
"""
|
||||
script_input(["report", "off"])
|
||||
main()
|
||||
assert FULL_REPORT in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_unknown_input_is_rejected(
|
||||
script_input: ScriptInput, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""!
|
||||
@brief Anything that is not a drink or command prints a message.
|
||||
"""
|
||||
script_input(["tea", "off"])
|
||||
main()
|
||||
assert "Sorry, we do not serve that." in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_sale_updates_resources_and_profit(
|
||||
script_input: ScriptInput, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""!
|
||||
@brief A paid drink is served, resources drop and profit rises by the price.
|
||||
"""
|
||||
script_input(["latte", *COINS_FOR_LATTE, "report", "off"])
|
||||
main()
|
||||
out = capsys.readouterr().out
|
||||
assert "Here is your latte. Enjoy!" in out
|
||||
assert "Water: 100ml\nMilk: 50ml\nCoffee: 76g\nMoney: $2.50" in out
|
||||
|
||||
|
||||
def test_refund_changes_nothing(
|
||||
script_input: ScriptInput, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""!
|
||||
@brief An underpaid order is refunded: no drink, no profit.
|
||||
"""
|
||||
script_input(["espresso", *NO_COINS, "report", "off"])
|
||||
main()
|
||||
out = capsys.readouterr().out
|
||||
assert "Money refunded." in out
|
||||
assert "Enjoy" not in out
|
||||
assert FULL_REPORT in out
|
||||
|
||||
|
||||
def test_overpayment_adds_only_price_to_profit(
|
||||
script_input: ScriptInput, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""!
|
||||
@brief Change is returned and only the price is counted as profit.
|
||||
"""
|
||||
script_input(["espresso", "7", "0", "0", "0", "report", "off"])
|
||||
main()
|
||||
out = capsys.readouterr().out
|
||||
assert "Here is $0.25 in change." in out
|
||||
assert "Money: $1.50" in out
|
||||
|
||||
|
||||
def test_order_refused_when_resources_run_out(
|
||||
script_input: ScriptInput, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""!
|
||||
@brief After the water is used up, the next order is refused before payment.
|
||||
"""
|
||||
script_input(["latte", *COINS_FOR_LATTE, "latte", "off"])
|
||||
main()
|
||||
out = capsys.readouterr().out
|
||||
assert out.count("Please insert coins.") == 1
|
||||
assert "Sorry there is not enough water." in out
|
||||
@@ -0,0 +1,52 @@
|
||||
"""!
|
||||
@file test_resources.py
|
||||
@brief Tests for the report and the resource check.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from coffee_machine.constants import INITIAL_RESOURCES, MENU
|
||||
from coffee_machine.main import format_money, is_resource_sufficient, report
|
||||
|
||||
|
||||
def test_format_money_shows_two_decimals() -> None:
|
||||
"""!
|
||||
@brief Cents are shown as dollars with two decimals.
|
||||
"""
|
||||
assert format_money(250) == "$2.50"
|
||||
assert format_money(0) == "$0.00"
|
||||
|
||||
|
||||
def test_report_prints_resources_and_money(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""!
|
||||
@brief The report lists each resource with its unit, then the money.
|
||||
"""
|
||||
report({"water": 300, "milk": 200, "coffee": 100}, 250)
|
||||
assert capsys.readouterr().out == (
|
||||
"Water: 300ml\nMilk: 200ml\nCoffee: 100g\nMoney: $2.50\n"
|
||||
)
|
||||
|
||||
|
||||
def test_is_resource_sufficient_when_enough() -> None:
|
||||
"""!
|
||||
@brief An order is accepted when every ingredient is available.
|
||||
"""
|
||||
assert is_resource_sufficient(MENU["latte"]["ingredients"], INITIAL_RESOURCES)
|
||||
|
||||
|
||||
def test_is_resource_sufficient_when_exactly_enough() -> None:
|
||||
"""!
|
||||
@brief Using up an ingredient completely is allowed.
|
||||
"""
|
||||
assert is_resource_sufficient({"water": 300}, {"water": 300})
|
||||
|
||||
|
||||
def test_is_resource_sufficient_names_missing_resource(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""!
|
||||
@brief A short ingredient is refused and named.
|
||||
"""
|
||||
resources = {"water": 300, "milk": 100, "coffee": 100}
|
||||
assert not is_resource_sufficient(MENU["latte"]["ingredients"], resources)
|
||||
assert capsys.readouterr().out == "Sorry there is not enough milk.\n"
|
||||
@@ -0,0 +1,56 @@
|
||||
"""!
|
||||
@file test_transactions.py
|
||||
@brief Tests for coin processing and payment.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from coffee_machine.main import is_transaction_successful, process_coins
|
||||
from tests.conftest import ScriptInput
|
||||
|
||||
|
||||
def test_process_coins_totals_in_cents(script_input: ScriptInput) -> None:
|
||||
"""!
|
||||
@brief Quarters, dimes, nickels and pennies add up to the right total.
|
||||
"""
|
||||
script_input(["4", "3", "2", "1"])
|
||||
assert process_coins() == 4 * 25 + 3 * 10 + 2 * 5 + 1
|
||||
|
||||
|
||||
def test_process_coins_asks_again_on_invalid_count(
|
||||
script_input: ScriptInput, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""!
|
||||
@brief Text and negative counts are rejected and asked again.
|
||||
"""
|
||||
script_input(["abc", "-1", "2", "0", "0", "0"])
|
||||
assert process_coins() == 50
|
||||
assert capsys.readouterr().out.count("Please enter a whole number") == 2
|
||||
|
||||
|
||||
def test_transaction_refused_when_too_little(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""!
|
||||
@brief Too little money is refunded with the refund message.
|
||||
"""
|
||||
assert not is_transaction_successful(100, 150)
|
||||
assert "Sorry that's not enough money. Money refunded." in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_transaction_accepted_with_exact_payment(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""!
|
||||
@brief Exact payment succeeds and gives no change.
|
||||
"""
|
||||
assert is_transaction_successful(150, 150)
|
||||
assert "change" not in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_transaction_gives_change(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""!
|
||||
@brief Overpaying succeeds and returns the difference.
|
||||
"""
|
||||
assert is_transaction_successful(175, 150)
|
||||
assert capsys.readouterr().out == "Here is $0.25 in change.\n"
|
||||
Reference in New Issue
Block a user