diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..81fe8defe --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# ========================= +# Python +# ========================= +__pycache__/ +*.py[cod] +*.pyo + +# ========================= +# Pytest +# ========================= +.pytest_cache/ + +# Игнорируем всё остальное, кроме htmlcov +.coverage +htmlcov/* +!htmlcov/index.html \ No newline at end of file diff --git a/README.md b/README.md index 272081708..8439ed85e 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,21 @@ Созданы юнит-тесты, покрывающие классы `Bun`, `Burger`, `Ingredient`, `Database` -Процент покрытия 100% (отчет: `htmlcov/index.html`) +Процент покрытия 100% (без main()) (отчет: `htmlcov/index.html`) ### Структура проекта - `praktikum` - пакет, содержащий код программы - `tests` - пакет, содержащий тесты, разделенные по классам. Например, `bun_test.py`, `burger_test.py` и т.д. +## Тесты + +Тесты покрывают все классы и используют: +- `pytest` для запуска +- `pytest-mock` для моков объектов (`Bun`, `Ingredient`) +- параметризацию (`@pytest.mark.parametrize`) для тестирования нескольких сценариев + + ### Запуск автотестов **Установка зависимостей** diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..e68beb4f4 --- /dev/null +++ b/conftest.py @@ -0,0 +1,72 @@ +import pytest +from praktikum.bun import Bun +from praktikum.ingredient import Ingredient +from praktikum.burger import Burger +from praktikum.database import Database +from test_data import * +from praktikum.ingredient_types import INGREDIENT_TYPE_SAUCE +from unittest.mock import Mock + + +@pytest.fixture +def bun(): + return Bun(BUN_NAME, BUN_PRICE) + +@pytest.fixture +def ingredient_factory(): + def _create(ingredient_type, name, price): + return Ingredient(ingredient_type, name, price) + return _create + +@pytest.fixture +def sauce_ingredient(ingredient_factory): + return ingredient_factory( + INGREDIENT_TYPE_SAUCE, + INGREDIENT_NAME, + INGREDIENT_PRICE + ) + +@pytest.fixture +def filling_ingredient(ingredient_factory): + return ingredient_factory(INGREDIENT_TYPE_FILLING, FILLING_NAME, FILLING_PRICE) + +@pytest.fixture +def burger() -> Burger: + return Burger() + +@pytest.fixture +def db() -> Database: + return Database() + +@pytest.fixture +def mock_bun(): + bun = Mock() + bun.get_name.return_value = MOCK_BUN_NAME + bun.get_price.return_value = MOCK_BUN_PRICE + return bun + +@pytest.fixture +def mock_sauce(): + ingredient = Mock() + ingredient.get_type.return_value = INGREDIENT_TYPE_SAUCE + ingredient.get_name.return_value = SAUCE_NAME + ingredient.get_price.return_value = SAUCE_PRICE + return ingredient + +@pytest.fixture +def mock_filling(): + ingredient = Mock() + ingredient.get_type.return_value = INGREDIENT_TYPE_FILLING + ingredient.get_name.return_value = FILLING_NAME + ingredient.get_price.return_value = FILLING_PRICE + return ingredient + +@pytest.fixture +def mock_ingredient_factory(): + def _create(ingredient_type, name, price): + ingredient = Mock(spec=["get_type", "get_name", "get_price"]) + ingredient.get_type.return_value = ingredient_type + ingredient.get_name.return_value = name + ingredient.get_price.return_value = price + return ingredient + return _create diff --git "a/praktikum/__init__.py\342\200\216" "b/praktikum/__init__.py\342\200\216" new file mode 100644 index 000000000..e69de29bb diff --git a/bun.py b/praktikum/bun.py similarity index 100% rename from bun.py rename to praktikum/bun.py diff --git a/burger.py b/praktikum/burger.py similarity index 99% rename from burger.py rename to praktikum/burger.py index 2b3b6a88b..20ca67a94 100644 --- a/burger.py +++ b/praktikum/burger.py @@ -12,6 +12,7 @@ class Burger: Можно распечать чек с информацией о бургере. """ + def __init__(self): self.bun = None self.ingredients: List[Ingredient] = [] diff --git a/database.py b/praktikum/database.py similarity index 100% rename from database.py rename to praktikum/database.py diff --git a/ingredient.py b/praktikum/ingredient.py similarity index 100% rename from ingredient.py rename to praktikum/ingredient.py diff --git a/ingredient_types.py b/praktikum/ingredient_types.py similarity index 100% rename from ingredient_types.py rename to praktikum/ingredient_types.py diff --git a/praktikum.py b/praktikum/praktikum.py similarity index 100% rename from praktikum.py rename to praktikum/praktikum.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..e68d4f2a4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +allure-pytest==2.15.3 +allure-python-commons==2.15.3 +attrs==25.4.0 +certifi==2026.1.4 +charset-normalizer==3.4.4 +coverage==7.13.0 +Faker==40.1.2 +h11==0.16.0 +idna==3.11 +iniconfig==2.3.0 +outcome==1.3.0.post0 +packaging==25.0 +pluggy==1.6.0 +Pygments==2.19.2 +PySocks==1.7.1 +pytest==9.0.2 +pytest-cov==7.0.0 +requests==2.32.5 +selenium==4.39.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +trio==0.32.0 +trio-websocket==0.12.2 +typing_extensions==4.15.0 +urllib3==2.6.3 +websocket-client==1.9.0 +wsproto==1.3.2 diff --git a/test_data.py b/test_data.py new file mode 100644 index 000000000..724564719 --- /dev/null +++ b/test_data.py @@ -0,0 +1,26 @@ +from praktikum.ingredient_types import INGREDIENT_TYPE_FILLING + +BUN_NAME = "Космическая булочка" +BUN_PRICE = 1150.0 + +INGREDIENT_TYPE = "SAUCE" +INGREDIENT_NAME = "Соус Spicy-X" +INGREDIENT_PRICE = 888.8 + +SAUCE_NAME = "Соус с шипами Антарианского плоскоходца" +SAUCE_PRICE = 88 + +SAUCE_NAME_2 = "Соус фирменный Space Sauce" +SAUCE_PRICE_2 = 13 + +FILLING_NAME = "Биокотлета из марсианской Магнолии" +FILLING_PRICE = 424 + +# Mock Bun +MOCK_BUN_NAME = "Космическая булочка (поджаренная)" +MOCK_BUN_PRICE = 431.00 + +# Mock Ingredient +MOCK_INGREDIENT_TYPE = INGREDIENT_TYPE_FILLING +MOCK_INGREDIENT_NAME = "Биокотлета из марсианской Магнолии (роскошная)" +MOCK_INGREDIENT_PRICE = 666.00 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_bun.py b/tests/test_bun.py new file mode 100644 index 000000000..d7983874b --- /dev/null +++ b/tests/test_bun.py @@ -0,0 +1,10 @@ +from praktikum.bun import Bun +from test_data import BUN_NAME, BUN_PRICE + +class TestBun: + + def test_get_name_returns_correct_name(self, bun: Bun): + assert bun.get_name() == BUN_NAME + + def test_get_price_returns_correct_price(self, bun: Bun): + assert bun.get_price() == BUN_PRICE diff --git a/tests/test_burger.py b/tests/test_burger.py new file mode 100644 index 000000000..b7e7cb067 --- /dev/null +++ b/tests/test_burger.py @@ -0,0 +1,88 @@ +from praktikum.burger import Burger +from test_data import * +from praktikum.ingredient_types import INGREDIENT_TYPE_SAUCE, INGREDIENT_TYPE_FILLING +import pytest + + +class TestBurger: + + def test_set_buns(self, burger: Burger, mock_bun): + burger.set_buns(mock_bun) + assert burger.bun == mock_bun + + def test_add_single_ingredient(self, burger: Burger, mock_sauce): + burger.add_ingredient(mock_sauce) + assert burger.ingredients == [mock_sauce] + + def test_remove_sauce_ingredient(self, burger: Burger, mock_sauce): + burger.add_ingredient(mock_sauce) + burger.remove_ingredient(0) + assert burger.ingredients == [] + + def test_move_single_ingredient_no_change(self, burger: Burger, mock_sauce): + burger.add_ingredient(mock_sauce) + + burger.move_ingredient(0, 0) + assert burger.ingredients == [mock_sauce] + + @pytest.mark.parametrize( + "start_index, new_index, expected_order", + [ + (0, 2, ["FILLING", "SAUCE", "SAUCE"]), + (2, 0, ["SAUCE", "SAUCE", "FILLING"]), + (1, 1, ["SAUCE", "FILLING", "SAUCE"]), + ] +) + def test_move_ingredient_various_positions( + self, + burger: Burger, + mock_ingredient_factory, + start_index, + new_index, + expected_order + ): + ing1 = mock_ingredient_factory(INGREDIENT_TYPE_SAUCE, SAUCE_NAME, SAUCE_PRICE) + ing2 = mock_ingredient_factory(INGREDIENT_TYPE_FILLING, FILLING_NAME, FILLING_PRICE) + ing3 = mock_ingredient_factory(INGREDIENT_TYPE_SAUCE, SAUCE_NAME_2, SAUCE_PRICE_2) + + for ing in [ing1, ing2, ing3]: + burger.add_ingredient(ing) + + burger.move_ingredient(start_index, new_index) + + remaining_types = [i.get_type() for i in burger.ingredients] + assert remaining_types == expected_order + + def test_get_price_single_ingredient(self, burger: Burger, mock_bun, mock_sauce): + + burger.set_buns(mock_bun) + burger.add_ingredient(mock_sauce) + + expected_price = mock_bun.get_price() * 2 + mock_sauce.get_price() + + assert burger.get_price() == expected_price + + def test_burger_get_price_multiple_ingredients(self, burger: Burger, mock_bun, mock_sauce, mock_filling): + burger.set_buns(mock_bun) + + burger.add_ingredient(mock_sauce) + burger.add_ingredient(mock_filling) + + expected_price = mock_bun.get_price() * 2 + mock_sauce.get_price() + mock_filling.get_price() + assert burger.get_price() == expected_price + + def test_get_receipt_single_ingredient1(self, burger: Burger, mock_bun, mock_sauce): + burger.set_buns(mock_bun) + burger.add_ingredient(mock_sauce) + + receipt = burger.get_receipt() + + expected_lines = [ + f"(==== {MOCK_BUN_NAME} ====)", + f"= sauce {SAUCE_NAME} =", + f"(==== {MOCK_BUN_NAME} ====)", + "", + f"Price: {burger.get_price()}" + ] + + assert receipt.split("\n") == expected_lines diff --git a/tests/test_database.py b/tests/test_database.py new file mode 100644 index 000000000..fdcfddb69 --- /dev/null +++ b/tests/test_database.py @@ -0,0 +1,32 @@ +from praktikum.database import Database + + +class TestDatabase: + + def test_database_buns_returns_all_buns(self, db: Database, mock_bun): + + db.buns = [mock_bun, mock_bun, mock_bun] + + buns = db.available_buns() + + assert len(buns) == 3 + assert buns[0].get_name() == mock_bun.get_name() + assert buns[0].get_price() == mock_bun.get_price() + + def test_available_ingredients(self, db: Database, mock_sauce, mock_filling): + + db.ingredients = [mock_sauce, mock_filling] + + ingredients = db.available_ingredients() + + assert len(ingredients) == 2 + assert ingredients[0] is mock_sauce + assert ingredients[0].get_type() == mock_sauce.get_type() + assert ingredients[0].get_name() == mock_sauce.get_name() + assert ingredients[0].get_price() == mock_sauce.get_price() + + assert ingredients[1] is mock_filling + assert ingredients[1].get_type() == mock_filling.get_type() + assert ingredients[1].get_name() == mock_filling.get_name() + assert ingredients[1].get_price() == mock_filling.get_price() + diff --git a/tests/test_ingredient.py b/tests/test_ingredient.py new file mode 100644 index 000000000..3db877340 --- /dev/null +++ b/tests/test_ingredient.py @@ -0,0 +1,40 @@ +from praktikum.ingredient import Ingredient +from test_data import INGREDIENT_NAME, INGREDIENT_PRICE, FILLING_NAME, FILLING_PRICE, SAUCE_NAME, SAUCE_PRICE +from praktikum.ingredient_types import INGREDIENT_TYPE_SAUCE, INGREDIENT_TYPE_FILLING +import pytest + + +class TestIngredient: + + def test_get_name_returns_correct_name(self, sauce_ingredient: Ingredient): + assert sauce_ingredient.get_name() == INGREDIENT_NAME + + def test_get_price_returns_correct_price(self, sauce_ingredient: Ingredient): + assert sauce_ingredient.get_price() == INGREDIENT_PRICE + + def test_get_type_returns_sauce_type(self, sauce_ingredient: Ingredient): + assert sauce_ingredient.get_type() == INGREDIENT_TYPE_SAUCE + + def test_get_type_returns_filling_type(self, filling_ingredient: Ingredient): + assert filling_ingredient.get_type() == INGREDIENT_TYPE_FILLING + + @pytest.mark.parametrize( + "ingredient_type, name, price, expected_type", + [ + (INGREDIENT_TYPE_SAUCE, SAUCE_NAME, SAUCE_PRICE, INGREDIENT_TYPE_SAUCE), + (INGREDIENT_TYPE_FILLING, FILLING_NAME, FILLING_PRICE, INGREDIENT_TYPE_FILLING), + ] + ) + def test_ingredient_properties( + self, + ingredient_factory, + ingredient_type, + name, + price, + expected_type + ): + ingredient = ingredient_factory(ingredient_type, name, price) + + assert ingredient.get_name() == name + assert ingredient.get_price() == price + assert ingredient.get_type() == expected_type