diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f9539a38e --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# Игнорируем виртуальные окружения +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Node.js зависимости +node_modules/ + +# Артефакты сборки +dist/ +build/ + +# Генерируемые файлы +*.pyc +*.log +*.json + +# IDE и системные файлы +.idea/ +.vscode/ +.DS_Store +Thumbs.db + +#Прочее +*.txt +__pycache__ +.pytest_cache +tests/__pycache__/ + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.htmlcov +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec \ No newline at end of file diff --git a/README.md b/README.md index 272081708..d23c5f8ac 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,72 @@ ## Задание 1: Юнит-тесты +# Тестирование веб-приложения https://stellarburgers.education-services.ru/ , которая помогает заказать бургер в Stellar Burgers +В данном проекте реализованы автоматизированные тесты с использованием фреймворка pytest. -### Автотесты для проверки программы, которая помогает заказать бургер в Stellar Burgers +--- +## Структура проекта +### `praktikum` - пакет, содержащий код программы -### Реализованные сценарии +### Тесты в директории `tests` -Созданы юнит-тесты, покрывающие классы `Bun`, `Burger`, `Ingredient`, `Database` +## Тестируются основные функции класса `Burger` +- Добавление булочек в бургер (`set_buns`) +- Добавление ингрелиентов в бургер (`add_ingredient`) +- Удаление ингредиентов (по индексу) (`remove_ingredient`) +- Перемещение ингредиентов (`move_ingredient`) +- Получение цены бургера (`get_price`) +- Вывод рецепта бургера (`get_receipt`) -Процент покрытия 100% (отчет: `htmlcov/index.html`) +## Описание тестов -### Структура проекта +### 1. `test_set_buns` +Проверяет, что в бургер успешно добавляются булочки. -- `praktikum` - пакет, содержащий код программы -- `tests` - пакет, содержащий тесты, разделенные по классам. Например, `bun_test.py`, `burger_test.py` и т.д. +### 2.1. `test_add_ingredient` +Проверяет, что в бургер успешно добавляются ингредиенты. -### Запуск автотестов +### 2.2. `test_add_multiple_ingredients_param` +Убеждается, что в бургер можно добавить несколько ингредиентов -**Установка зависимостей** +### 3.1 `test_remove_ingredient` +Проверяет, что можно удалить необходимый ингредиент -> `$ pip install -r requirements.txt` +### 3.2. `test_remove_ingredient_invalid_index` +Проверяет, что нельзя удалить недобавленный ингредиент(с несуществующем индексом) + +### 3.3. `test_remove_from_empty_burger` +Убеждается, что в пустом бургере (без ингредиентов) нечего удалять + +### 4. `test_move_ingredient_param` +Проверяет, что ингредиенты успешно перемещаются внутри бургера + +### 5. `test_get_price` +Гарантирует, что цену возможно вывести цену + +### 6. `test_get_price_param` +Проверяет, что цену можно получить и с несколькими ингредиентами + +### 7. `test_get_receipt` +Проверяет, что получаемый рецепт соответствует эталону + +--- + +## Описание остальных файлов + +**conftest.py** +Содержит фикстуры для создания моков + +**data.py** +Содержит данные для тестов + +--- + +## Как запустить тесты +pytest -v + +## Как установить все зависимости +pip install -r requirements.txt + +## Отчет о покрытии +pytest --cov=praktikum --cov-report=html -**Запуск автотестов и создание HTML-отчета о покрытии** -> `$ pytest --cov=praktikum --cov-report=html` diff --git a/__init__.py b/praktikum/__init__.py similarity index 100% rename from __init__.py rename to praktikum/__init__.py 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/praktikum.py b/praktikum/praktikum.py similarity index 100% rename from praktikum.py rename to praktikum/praktikum.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..091d6bbfb --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,39 @@ +import pytest +from unittest.mock import Mock +from praktikum.burger import Burger + + +@pytest.fixture +def create_mock_bun(): + """Фикстура возвращает мок булок""" + def _create_mock_bun(name, price): + mock_bun = Mock() + mock_bun.get_name.return_value = name + mock_bun.get_price.return_value = price + return mock_bun + return _create_mock_bun # возвращаем внутреннюю функцию + +@pytest.fixture +def create_mock_ingredient(): + """Фикстура возвращает мок ингредиентов""" + def _create_mock_ingredient(type, name, price): + mock_ingredient = Mock() + mock_ingredient.get_type.return_value = type + mock_ingredient.get_name.return_value = name + mock_ingredient.get_price.return_value = price + return mock_ingredient + return _create_mock_ingredient + +@pytest.fixture +def create_mock_burger(create_mock_bun, create_mock_ingredient): + """Фикстура создает бургер""" + def _create_mock_burger(bun_name, ingredients_data): + bun_mock = create_mock_bun(bun_name, 0) + burger = Burger() + burger.set_buns(bun_mock) + + for ing_type, ing_name in ingredients_data: + ing_mock = create_mock_ingredient(ing_type, ing_name, 0) + burger.add_ingredient(ing_mock) + return burger + return _create_mock_burger diff --git a/tests/data.py b/tests/data.py new file mode 100644 index 000000000..2acbd9873 --- /dev/null +++ b/tests/data.py @@ -0,0 +1,48 @@ +from praktikum.ingredient_types import * + + +class Credentials: + bun_names = ["black bun", "white bun", "red bun", "чёрная булка", "белая булка", "красная булка"] + bun_prices = [100, 200, 300, 101, 99, 109, 201, 299, 301] + + ingredient_combination = [ + (INGREDIENT_TYPE_SAUCE, "hot sauce", 100), + (INGREDIENT_TYPE_SAUCE, "sour cream", 200), + (INGREDIENT_TYPE_SAUCE, "chili sauce", 300), + (INGREDIENT_TYPE_FILLING, "cutlet", 100), + (INGREDIENT_TYPE_FILLING, "dinosaur", 200), + (INGREDIENT_TYPE_FILLING, "sausage", 300)] + + receipt_data = [ + ("black bun", + [(INGREDIENT_TYPE_SAUCE, "hot sauce")], + 300, + "(==== black bun ====)\n= sauce hot sauce =\n(==== black bun ====)\n\nPrice: 300" + ), + ("white bun", + [(INGREDIENT_TYPE_FILLING, "cutlet")], + 400, + "(==== white bun ====)\n= filling cutlet =\n(==== white bun ====)\n\nPrice: 400" + ), + ("red bun", + [(INGREDIENT_TYPE_SAUCE, "sour cream"), (INGREDIENT_TYPE_FILLING, "dinosaur")], + 500, + "(==== red bun ====)\n= sauce sour cream =\n= filling dinosaur =\n(==== red bun ====)\n\nPrice: 500" + ), + ("black bun", + [ + (INGREDIENT_TYPE_SAUCE, "hot sauce"), + (INGREDIENT_TYPE_FILLING, "cutlet"), + (INGREDIENT_TYPE_SAUCE, "chili sauce"), + (INGREDIENT_TYPE_FILLING, "dinosaur") + ], + 1000, + "(==== black bun ====)\n= sauce hot sauce =\n= filling cutlet =\n= sauce chili sauce =\n= filling dinosaur =\n(==== black bun ====)\n\nPrice: 1000" + ), + ( + "white bun", + [], + 200, + "(==== white bun ====)\n(==== white bun ====)\n\nPrice: 200" + ) + ] diff --git a/tests/test_add_ingredient.py b/tests/test_add_ingredient.py new file mode 100644 index 000000000..38260c4fe --- /dev/null +++ b/tests/test_add_ingredient.py @@ -0,0 +1,46 @@ +import pytest +from praktikum.burger import Burger +from praktikum.ingredient_types import INGREDIENT_TYPE_SAUCE, INGREDIENT_TYPE_FILLING +from tests.data import Credentials + + +class TestAddIngredient: + ''' + Проверяется метод добавления ингредиентов + def add_ingredient(self, ingredient: Ingredient): + self.ingredients.append(ingredient) + ''' + + @pytest.mark.parametrize("ingredient_type, ingredient_name, ingredient_price", Credentials.ingredient_combination) + def test_add_ingredient(self, ingredient_type, ingredient_name, ingredient_price, create_mock_ingredient): + + mok_ingredient = create_mock_ingredient(ingredient_type, ingredient_name, ingredient_price) + burger = Burger() + burger.add_ingredient(mok_ingredient) + + assert len(burger.ingredients) == 1, "Ингредиент не добавился в бургер" + + added_ingredient = burger.ingredients[0] #получить добавленный ингредиент + + assert added_ingredient.get_type() == ingredient_type, \ + f"Ожидался тип '{ingredient_type}', получен '{added_ingredient.get_type()}'" + assert added_ingredient.get_name() == ingredient_name, \ + f"Ожидалось имя '{ingredient_name}', получено '{added_ingredient.get_name()}'" + assert added_ingredient.get_price() == ingredient_price, \ + f"Ожидалась цена {ingredient_price}, получена {added_ingredient.get_price()}" + + #Проверяем, что можно добавить разное количество ингредиентов + @pytest.mark.parametrize("ingredients_count", [1, 2, 3, 4, 5]) + def test_add_multiple_ingredients_param(self, ingredients_count, create_mock_ingredient): + burger = Burger() + + for i in range(ingredients_count): + mock_ingredient = create_mock_ingredient( + INGREDIENT_TYPE_SAUCE, + f"sauce_{i}", + i * 100 + ) + burger.add_ingredient(mock_ingredient) + + assert len(burger.ingredients) == ingredients_count, \ + f"Ожидалось {ingredients_count} ингредиентов, получено {len(burger.ingredients)}" diff --git a/tests/test_get_price.py b/tests/test_get_price.py new file mode 100644 index 000000000..86d4336f9 --- /dev/null +++ b/tests/test_get_price.py @@ -0,0 +1,59 @@ +from unittest.mock import Mock +import pytest +from praktikum.burger import Burger + + +class TestGetPrice: + + '''Проверяется метод получения цены на бургер + def get_price(self) -> float: + price = self.bun.get_price() * 2 + + for ingredient in self.ingredients: + price += ingredient.get_price() + + return price + ''' + + @pytest.mark.parametrize("bun_price, ingredient_price, expected_price", [ + (100, 100, 300), + (200, 200, 600), + (300, 300, 900) + ]) + + def test_get_price(self, bun_price, ingredient_price, expected_price): + mock_bun = Mock() + mock_bun.get_price.return_value = bun_price + mock_ingredient = Mock() + mock_ingredient.get_price.return_value = ingredient_price + + burger = Burger() + burger.set_buns(mock_bun) + burger.add_ingredient(mock_ingredient) + price = burger.get_price() + + assert price == expected_price, \ + f"Ожидалась цена {expected_price}, но получена {price}" + + #несколько ингредиентов + @pytest.mark.parametrize("bun_price, ingredient_price, expected_price", [ + (100, [100, 100], 400), + (200, [200, 100], 700), + (300, [300, 200, 100], 1200) + ]) + def test_get_price_param(self, bun_price, ingredient_price, expected_price): + mock_bun = Mock() + mock_bun.get_price.return_value = bun_price + + burger = Burger() + burger.set_buns(mock_bun) + + for ing_price in ingredient_price: + mock_ingredient = Mock() + mock_ingredient.get_price.return_value = ing_price + burger.add_ingredient(mock_ingredient) + + price = burger.get_price() + + assert price == expected_price, \ + f"Ожидалась цена {expected_price}, но получена {price}" diff --git a/tests/test_get_receipt.py b/tests/test_get_receipt.py new file mode 100644 index 000000000..5dd1d7ae8 --- /dev/null +++ b/tests/test_get_receipt.py @@ -0,0 +1,31 @@ +from unittest.mock import patch +import pytest +from tests.data import Credentials +from praktikum.burger import Burger + + +class TestGetReceipt: + + '''Проверяется метод получения рецепта на бургер + def get_receipt(self) -> str: + receipt: List[str] = [f'(==== {self.bun.get_name()} ====)'] + + for ingredient in self.ingredients: + receipt.append(f'= {str(ingredient.get_type()).lower()} {ingredient.get_name()} =') + + receipt.append(f'(==== {self.bun.get_name()} ====)\n') + receipt.append(f'Price: {self.get_price()}') + + return '\n'.join(receipt) + ''' + + @pytest.mark.parametrize("bun_name, ingredients_data, mock_price, expected_receipt", Credentials.receipt_data) + @patch('praktikum.burger.Burger.get_price') + def test_get_receipt(self, mock_get_price, bun_name, ingredients_data, mock_price, expected_receipt, create_mock_burger): + + burger = create_mock_burger(bun_name, ingredients_data) + mock_get_price.return_value = mock_price + receipt = burger.get_receipt() + + assert receipt == expected_receipt + \ No newline at end of file diff --git a/tests/test_move_ingredient.py b/tests/test_move_ingredient.py new file mode 100644 index 000000000..069080f1b --- /dev/null +++ b/tests/test_move_ingredient.py @@ -0,0 +1,41 @@ +from unittest.mock import Mock +import pytest +from praktikum.burger import Burger + + +class TestRemoveIngredient: + + ''' + Проверяется метод перемещения ингредиентов + def move_ingredient(self, index: int, new_index: int): + self.ingredients.insert(new_index, self.ingredients.pop(index)) + ''' + + @pytest.mark.parametrize("move_from, move_to, expected_burger", [ + (0, 2, ["dinosaur", "cutlet", "sour cream"]), # первый в конец + (2, 0, ["cutlet", "sour cream", "dinosaur"]), # последний в начало + (1, 2, ["sour cream", "cutlet", "dinosaur"]), # средний в конец + (1, 0, ["dinosaur", "sour cream", "cutlet"]), # средний в начало + ]) + def test_move_ingredient_param(self, move_from, move_to, expected_burger): + burger = Burger() + + ingredients = [ + Mock(get_name=Mock(return_value="sour cream")), + Mock(get_name=Mock(return_value="dinosaur")), + Mock(get_name=Mock(return_value="cutlet")) + ] + + for ing in ingredients: + burger.add_ingredient(ing) + + burger.move_ingredient(move_from, move_to) + + result_names = [] + for ing in burger.ingredients: + name = ing.get_name() + result_names.append(name) + + assert result_names == expected_burger, \ + f"Ожидался порядок {expected_burger}, получен {result_names}" + \ No newline at end of file diff --git a/tests/test_remove_ingredient.py b/tests/test_remove_ingredient.py new file mode 100644 index 000000000..1b2d9e9e6 --- /dev/null +++ b/tests/test_remove_ingredient.py @@ -0,0 +1,49 @@ +from unittest.mock import Mock +import pytest +from praktikum.burger import Burger + + +class TestRemoveIngdient: + + + ''' + Проверяется метод удаления ингредиентов + def remove_ingredient(self, index: int): + del self.ingredients[index] + ''' + + def test_remove_ingredient(self): + burger = Burger() + + ingredients = [ + Mock(get_name=Mock(return_value="sour cream")), + Mock(get_name=Mock(return_value="dinosaur")), + Mock(get_name=Mock(return_value="cutlet")) + ] + + for ing in ingredients: + burger.add_ingredient(ing) + + burger.remove_ingredient(1) + + assert len(burger.ingredients) == 2, \ + f"ожидалось 2 ингредиента, получено {len(burger.ingredients)}" + assert burger.ingredients[0] is ingredients[0], \ + f"ожидалось sour cream" + assert burger.ingredients[1] is ingredients[2] , \ + f"ожидалось cutlet" + + #Проверяем ошибку при несуществующем индексе + def test_remove_ingredient_invalid_index(self): + burger = Burger() + burger.add_ingredient(Mock()) # один ингредиент + + with pytest.raises(IndexError): + burger.remove_ingredient(5) # ожидается ошибка по индексу + + #Проверяем ошибку при пустом бургере, без ингредиентов + def test_remove_from_empty_burger(self): + burger = Burger() + + with pytest.raises(IndexError): + burger.remove_ingredient(0) # ожидается ошибка по индексу diff --git a/tests/test_set_buns.py b/tests/test_set_buns.py new file mode 100644 index 000000000..1a7a27dfd --- /dev/null +++ b/tests/test_set_buns.py @@ -0,0 +1,25 @@ +import pytest +from praktikum.burger import Burger +from tests.data import Credentials + + +class TestSetBuns: + + ''' + Проверяется метод добавления булочек + def set_buns(self, bun: Bun): + self.bun = bun + ''' + @pytest.mark.parametrize("bun_name, bun_price", zip(Credentials.bun_names, Credentials.bun_prices)) + + def test_set_buns(self, bun_name, bun_price, create_mock_bun): + + bun_mock = create_mock_bun(bun_name, bun_price) + burger = Burger() + burger.set_buns(bun_mock) + + assert burger.bun is not None, "Булка не установлена" + assert burger.bun.get_name() == bun_name, \ + f"Ожидалось имя булки '{bun_name}', получено '{burger.bun.get_name()}'" + assert burger.bun.get_price() == bun_price,\ + f"Ожидалась цена булки '{bun_price}', получено '{burger.bun.get_price()}'"