diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..10c95f77c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +venv/ +__pycache__/ +.pytest_cache/ +.coverage +*.pyc \ No newline at end of file diff --git a/README.md b/README.md index 272081708..efcc4a532 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,14 @@ ### Реализованные сценарии -Созданы юнит-тесты, покрывающие классы `Bun`, `Burger`, `Ingredient`, `Database` +Созданы юнит-тесты, покрывающие класс `Burger` Процент покрытия 100% (отчет: `htmlcov/index.html`) ### Структура проекта - `praktikum` - пакет, содержащий код программы -- `tests` - пакет, содержащий тесты, разделенные по классам. Например, `bun_test.py`, `burger_test.py` и т.д. +- `tests` - пакет, содержащий тесты, разделенные по классам - `burger_test.py` ### Запуск автотестов @@ -21,4 +21,16 @@ **Запуск автотестов и создание HTML-отчета о покрытии** -> `$ pytest --cov=praktikum --cov-report=html` +> `python -m pytest --cov=praktikum.burger --cov-report=html` + + +`python -m pytest tests/burger_test.py -v` + + +![Покрытие 100%](image.png) + +Исправления по замечаниям ревьюера: +Принцип «Черного ящика»: Полностью исключена проверка внутренних атрибутов (self.bun, self.ingredients, len()). Все ассерты переписаны на проверку поведения через публичные методы get_price() и get_receipt(). +Атомарность тестов: Тесты разбиты на изолированные сценарии. Каждый тест проверяет одно конкретное поведение метода (влияние на цену, порядок в чеке, возникновение ошибки) без лишних промежуточных проверок состояния. +Покрытие: Достигнуто 100% покрытие кода класса Burger (отчет htmlcov сформирован, красных строк нет). +Структура: Удалены избыточные тесты для классов Bun, Ingredient и Database, оставлены только тесты для целевого класса Burger согласно требованиям задания. \ No newline at end of file diff --git a/image.png b/image.png new file mode 100644 index 000000000..742ece68c Binary files /dev/null and b/image.png differ diff --git a/praktikum/__init__.py b/praktikum/__init__.py 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 100% rename from burger.py rename to praktikum/burger.py 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/requirements.txt b/requirements.txt new file mode 100644 index 000000000..cffeec658 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pytest +pytest-cov \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/burger_test.py b/tests/burger_test.py new file mode 100644 index 000000000..8521cab3b --- /dev/null +++ b/tests/burger_test.py @@ -0,0 +1,145 @@ +from praktikum.burger import Burger +from praktikum.bun import Bun +from praktikum.ingredient import Ingredient + +from unittest.mock import Mock +import pytest + + + +class TestBurgerInitialization: + + # ИСПРАВЛЕННЫЙ ТЕСТ 1: Проверяем set_buns через влияние на цену + def test_set_bun_calculates_price_correctly(self, burger_instance, mock_bun): + # Arrange: mock_bun имеет цену 1000 (из фикстуры) + burger_instance.set_buns(mock_bun) + # Если булка установилась верно, цена должна быть 1000 * 2 = 2000. + # Если бы булка не установилась, метод get_price() упал бы с ошибкой (AttributeError). + assert burger_instance.get_price() == 2000 + + + # Тест 2 НОВЫЙ: Проверяем поведение метода add_ingredient через изменение цены + def test_add_ingredient_increases_price(self, burger_instance, mock_bun, mock_ingredient): + # Устанавливаем булку, чтобы цена считалась корректно (иначе будет ошибка или 0) + burger_instance.set_buns(mock_bun) + # Цена булки 1000 (из фикстуры), значит база = 2000. + # Цена ингредиента 50 (из фикстуры). Ожидаемый итог: 2050. + expected_price = 2050 + # Добавляем ингредиент + burger_instance.add_ingredient(mock_ingredient) + # Проверяем результат через публичный метод get_price() + assert burger_instance.get_price() == expected_price + + + # Тест 3 НОВЫЙ: Проверяем поведение метода add_ingredient формирование чека + def test_add_ingredient_appears_in_receipt(self, burger_instance, mock_bun, mock_ingredient): + # Устанавливаем булку + burger_instance.set_buns(mock_bun) + # Добавляем ингредиент + burger_instance.add_ingredient(mock_ingredient) + # Получаем чек + receipt = burger_instance.get_receipt() + # Проверяем, что имя ингредиента есть в чеке + # Мы знаем из фикстуры, что имя "Test Ingredient Name" + assert "Test Ingredient Name" in receipt + + + +class TestBurgerManipulation: + + # Тест 1 : Позитивный сценарий удаления ингредиента (проверка через изменение цены) + def test_remove_ingredient_decreases_price(self, burger_instance, mock_bun, mock_ingredient, mock_two_ingredient): + # Подготовка данных + # Из фикстур знаем: + # mock_ingredient price = 50 + # mock_two_ingredient price = 70 + burger_instance.set_buns(mock_bun) # Цена булки 1000 -> база 2000 + burger_instance.add_ingredient(mock_ingredient) # +50 -> 2050 + burger_instance.add_ingredient(mock_two_ingredient) # +70 -> 2120 + # Удаляем первый ингредиент (тот, что стоит 50) + burger_instance.remove_ingredient(0) + # Проверяем поведение через цену + # Ожидаемая цена: 2125 - 50 = 2070 + expected_price_after_remove = 2070 + assert burger_instance.get_price() == expected_price_after_remove + + + # Тест 2: Удаление (Негативный / Ошибка) + def test_remove_ingredient_raises_error_on_invalid_index(self, burger_instance, mock_ingredient): + burger_instance.add_ingredient(mock_ingredient) + with pytest.raises(IndexError): + burger_instance.remove_ingredient(99) + + # ИСПРАВЛЕННЫЙ ТЕСТ 3: Проверяем move_ingredient через порядок в чеке + def test_move_ingredient_changes_order_in_receipt(self, burger_instance, mock_ingredient_a, mock_ingredient_b, mock_ingredient_c, mock_bun): + # Берём данные из фикстуры (3 мока) + burger_instance.set_buns(mock_bun) + burger_instance.add_ingredient(mock_ingredient_a) # Порядок: A, B, C + burger_instance.add_ingredient(mock_ingredient_b) + burger_instance.add_ingredient(mock_ingredient_c) + # Перемещаем A (индекс 0) в конец (индекс 2). Ожидаемый порядок: B, C, A + burger_instance.move_ingredient(0, 2) + # Получаем чек и проверяем порядок следования имен в строке + receipt = burger_instance.get_receipt() + # Находим индексы вхождений имен в строку чека + pos_a = receipt.find("Ingredient_A") + pos_b = receipt.find("Ingredient_B") + pos_c = receipt.find("Ingredient_C") + # Проверяем, что все найдены и порядок верный: B идет раньше C, C раньше A + assert pos_b < pos_c < pos_a + + + # Тест 4: Перемещение (Негативный / Ошибка) + def test_move_ingredient_raises_error_on_invalid_index(self, burger_instance, mock_ingredient): + burger_instance.add_ingredient(mock_ingredient) + with pytest.raises(IndexError): + burger_instance.move_ingredient(5, 0) + + +class TestBurgerPrice: + + @pytest.mark.parametrize("bun_price, ingredient_prices, expected_total", [ + (100, [], 200), + (150, [50], 350), + (200, [10, 20], 430),]) + + def test_get_price_calculation(self, burger_instance, bun_price, ingredient_prices, expected_total): + mock_bun = Mock() + mock_bun.get_price.return_value = bun_price + mock_bun.get_name.return_value = "Test Bun" + burger_instance.set_buns(mock_bun) + for price in ingredient_prices: + mock_ingredient = Mock() + mock_ingredient.get_price.return_value = price + mock_ingredient.get_type.return_value = "FILLING" + mock_ingredient.get_name.return_value = "Test Ingredient" + burger_instance.add_ingredient(mock_ingredient) + actual_price = burger_instance.get_price() + assert actual_price == expected_total + + +class TestBurgerReceipt: + + + @pytest.mark.parametrize("bun_name, ing_type, ing_name, expected_ing_type_lower",[ + ('White Bun', "FILLING", "Meat Ball", "filling" ), + ("Black Bun", "SAUCE", "Hot Sauce", "sauce"), + ("Red Bun", "SaUcE", "Ketchup","sauce"), + ]) + def test_get_receipt_format(self, burger_instance, bun_name, ing_name, ing_type, expected_ing_type_lower): + mock_bun = Mock() + mock_bun.get_name.return_value = bun_name + mock_bun.get_price.return_value = 1500 + + mock_ingredient = Mock() + mock_ingredient.get_type.return_value = ing_type + mock_ingredient.get_name.return_value = ing_name + mock_ingredient.get_price.return_value = 420 + + expected_price = (1500*2) + 420 + + burger_instance.set_buns(mock_bun) + burger_instance.add_ingredient(mock_ingredient) + expected_receipt = f"(==== {bun_name} ====)\n= {expected_ing_type_lower} {ing_name} =\n(==== {bun_name} ====)\n\nPrice: {expected_price}" + actual_receipt = burger_instance.get_receipt() + assert actual_receipt == expected_receipt diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..2178dbd55 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,66 @@ +from praktikum.burger import Burger +from praktikum.bun import Bun +from praktikum.ingredient import Ingredient + +from unittest.mock import Mock +import pytest + + +@pytest.fixture +def burger_instance(): + burger = Burger() + return burger + + +@pytest.fixture +def mock_bun(): + bun_mock = Mock() + bun_mock.get_name.return_value = 'Test Bun Name' + bun_mock.get_price.return_value = 1000 + return bun_mock + + +@pytest.fixture +def mock_ingredient(): + ingredient_mock = Mock() + ingredient_mock.get_type.return_value = "FILLING" + ingredient_mock.get_name.return_value = "Test Ingredient Name" + ingredient_mock.get_price.return_value = 50 + return ingredient_mock + + +@pytest.fixture +def mock_two_ingredient(): + ingredient_two_mock = Mock() + ingredient_two_mock.get_type.return_value = "SAUCE" + ingredient_two_mock.get_name.return_value = "Test Ingredient Name" + ingredient_two_mock.get_price.return_value = 70 + return ingredient_two_mock + + +@pytest.fixture +def mock_ingredient_a(): + ing_a = Mock() + ing_a.get_name.return_value = "Ingredient_A" + ing_a.get_type.return_value = "FILLING" + ing_a.get_price.return_value = 10 + return ing_a + + +@pytest.fixture +def mock_ingredient_b(): + ing_b = Mock() + ing_b.get_name.return_value = "Ingredient_B" + ing_b.get_type.return_value = "FILLING" + ing_b.get_price.return_value = 10 + return ing_b + + +@pytest.fixture +def mock_ingredient_c(): + ing_c = Mock() + ing_c.get_name.return_value = "Ingredient_C" + ing_c.get_type.return_value = "FILLING" + ing_c.get_price.return_value = 10 + return ing_c +