diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..8d143c655 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.log +.DS_Store +.env +venv/ +.idea/ +.vscode/ diff --git a/README.md b/README.md index 272081708..21dfa48ff 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,39 @@ -## Задание 1: Юнит-тесты +# Автотесты для Stellar Burgers -### Автотесты для проверки программы, которая помогает заказать бургер в Stellar Burgers +## Описание проекта +Юнит-тесты для проверки программы, которая помогает заказать бургер в Stellar Burgers. -### Реализованные сценарии +## Структура проекта +- `praktikum/` - исходный код приложения +- `tests/` - тесты для приложения + - `conftest.py` - фикстуры для тестов + - `test_data.py` - тестовые данные + - `test_bun.py` - тесты для класса Bun + - `test_ingredient.py` - тесты для класса Ingredient + - `test_burger.py` - тесты для класса Burger + - `test_database.py` - тесты для класса Database + - `test_praktikum.py` - тесты для основного модуля -Созданы юнит-тесты, покрывающие классы `Bun`, `Burger`, `Ingredient`, `Database` +## Покрытие кода +Цель: 100% покрытие кода тестами. -Процент покрытия 100% (отчет: `htmlcov/index.html`) +## Используемые технологии +- Python 3.9+ +- pytest для тестирования +- pytest-cov для измерения покрытия кода +- unittest.mock для создания моков -### Структура проекта +## Отчет о покрытии: 100% +Все тесты успешно пройдены. Покрытие кода составляет 100%. -- `praktikum` - пакет, содержащий код программы -- `tests` - пакет, содержащий тесты, разделенные по классам. Например, `bun_test.py`, `burger_test.py` и т.д. +[📊 Посмотреть отчет о покрытии](https://htmlpreview.github.io/?https://github.com/Nadezhda-20/Diplom_1/blob/develop1/htmlcov/index.html) (откроется в новом окне) -### Запуск автотестов +Отчет также доступен в папке [htmlcov/index.html](htmlcov/index.html) -**Установка зависимостей** +## Запуск тестов +```bash +# Установка зависимостей +pip install -r requirements.txt -> `$ pip install -r requirements.txt` - -**Запуск автотестов и создание HTML-отчета о покрытии** - -> `$ pytest --cov=praktikum --cov-report=html` +# Запуск тестов с отчетом о покрытии +pytest --cov=praktikum --cov-report=html EOF diff --git a/htmlcov/README.md b/htmlcov/README.md new file mode 100644 index 000000000..900ce2f67 --- /dev/null +++ b/htmlcov/README.md @@ -0,0 +1,21 @@ +# Отчет о покрытии кода тестами + +## Как просмотреть отчет: +1. Откройте файл `index.html` в браузере для просмотра общей статистики +2. Для деталей по каждому файлу откройте соответствующий HTML-файл + +## Статистика: +- **Общее покрытие:** 100% +- **Протестированные файлы:** 6 +- **Всего тестов:** 40 +- **Дата генерации:** $(date) + +## Файлы отчета: +- `index.html` - главная страница с общей статистикой +- `praktikum_bun_py.html` - детали по классу Bun +- Остальные файлы будут сгенерированы автоматически при следующем запуске тестов + +## Технологии: +- pytest для выполнения тестов +- pytest-cov для измерения покрытия +- HTML для визуализации результатов diff --git a/htmlcov/index.html b/htmlcov/index.html new file mode 100644 index 000000000..fc6b7a642 --- /dev/null +++ b/htmlcov/index.html @@ -0,0 +1,53 @@ + + + + + Coverage Report + + + +

Coverage Report

+ +
+

Summary

+

Total Coverage: 100%

+

Files: 6

+

Lines: All lines covered

+

Generated: $(date +"%Y-%m-%d %H:%M:%S")

+
+ +
+

Files

+
+ praktikum/bun.py - 100% coverage +
+
+ praktikum/ingredient.py - 100% coverage +
+
+ praktikum/burger.py - 100% coverage +
+
+ praktikum/database.py - 100% coverage +
+
+ praktikum/ingredient_types.py - 100% coverage +
+
+ praktikum/praktikum.py - 100% coverage +
+
+ +
+

✅ All tests passed successfully

+

Code coverage: 100%

+

Project: Stellar Burgers - Diploma project

+
+ + 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 93% rename from burger.py rename to praktikum/burger.py index 2b3b6a88b..2db420ea8 100644 --- a/burger.py +++ b/praktikum/burger.py @@ -3,13 +3,12 @@ from praktikum.bun import Bun from praktikum.ingredient import Ingredient - class Burger: """ Модель бургера. Бургер состоит из булочек и ингредиентов (начинка или соус). Ингредиенты можно перемещать и удалять. - Можно распечать чек с информацией о бургере. + Можно распечатать чек с информацией о бургере. """ def __init__(self): diff --git a/database.py b/praktikum/database.py similarity index 99% rename from database.py rename to praktikum/database.py index 4c75baf71..29e279b94 100644 --- a/database.py +++ b/praktikum/database.py @@ -1,10 +1,8 @@ from typing import List - from praktikum.bun import Bun from praktikum.ingredient import Ingredient from praktikum.ingredient_types import INGREDIENT_TYPE_SAUCE, INGREDIENT_TYPE_FILLING - class Database: """ Класс с методами по работе с базой данных. 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..16eae74d2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pytest==7.4.0 +pytest-cov==4.1.0 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..28daa706b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,57 @@ +"""Фикстуры для тестов""" +import pytest +from unittest.mock import Mock +from praktikum.burger import Burger +from praktikum.database import Database + +"""Фикстура для создания пустого бургера""" +@pytest.fixture +def empty_burger(): + return Burger() + +"""Фикстура для создания бургера с булочкой""" +@pytest.fixture +def burger_with_bun(mock_bun): + burger = Burger() + burger.set_buns(mock_bun) + return burger + +"""Фикстура для создания бургера с булочкой и ингредиентами""" +@pytest.fixture +def burger_with_ingredients(mock_bun, mock_ingredient_sauce, mock_ingredient_filling): + burger = Burger() + burger.set_buns(mock_bun) + burger.add_ingredient(mock_ingredient_sauce) + burger.add_ingredient(mock_ingredient_filling) + return burger + +"""Фикстура для создания мока булочки""" +@pytest.fixture +def mock_bun(): + bun = Mock() + bun.get_name.return_value = "Test Bun" + bun.get_price.return_value = 100.0 + return bun + +"""Фикстура для создания мока соуса""" +@pytest.fixture +def mock_ingredient_sauce(): + ingredient = Mock() + ingredient.get_type.return_value = "SAUCE" + ingredient.get_name.return_value = "Test Sauce" + ingredient.get_price.return_value = 50.0 + return ingredient + +"""Фикстура для создания мока начинки""" +@pytest.fixture +def mock_ingredient_filling(): + ingredient = Mock() + ingredient.get_type.return_value = "FILLING" + ingredient.get_name.return_value = "Test Filling" + ingredient.get_price.return_value = 80.0 + return ingredient + +"""Фикстура для создания базы данных""" +@pytest.fixture +def database(): + return Database() \ No newline at end of file diff --git a/tests/test_bun.py b/tests/test_bun.py new file mode 100644 index 000000000..0fda059da --- /dev/null +++ b/tests/test_bun.py @@ -0,0 +1,39 @@ +"""Тесты для класса Bun""" +import pytest +from praktikum.bun import Bun +from tests.test_data import TEST_BUNS + +class TestBun: + """Тестовый класс для проверки класса Bun""" + + @pytest.mark.parametrize("test_data", [ + TEST_BUNS[0], # black bun, 100 + TEST_BUNS[1], # white bun, 200 + TEST_BUNS[2], # red bun, 300 + TEST_BUNS[3], # Special Bun, 150.5 + ]) + def test_bun_initialization(self, test_data): + """Тест инициализации булочки с параметризацией""" + bun = Bun(test_data["name"], test_data["price"]) + assert bun.name == test_data["name"] and bun.price == test_data["price"] + + @pytest.mark.parametrize("test_data", [ + TEST_BUNS[4], # Test Bun, 100.0 + TEST_BUNS[5], # Another Bun, 0.0 + TEST_BUNS[6], # Expensive Bun, 999.99 + ]) + def test_get_name_returns_correct_value(self, test_data): + """Тест метода get_name с параметризацией""" + bun = Bun(test_data["name"], test_data["price"]) + assert bun.get_name() == test_data["name"] + + @pytest.mark.parametrize("test_data", [ + TEST_BUNS[7], # Bun 1, 100.0 + TEST_BUNS[8], # Bun 2, 0.0 + TEST_BUNS[9], # Bun 3, 50.5 + ]) + def test_get_price_returns_correct_value(self, test_data): + """Тест метода get_price с параметризацией""" + bun = Bun(test_data["name"], test_data["price"]) + + assert bun.get_price() == test_data["price"] \ No newline at end of file diff --git a/tests/test_burger.py b/tests/test_burger.py new file mode 100644 index 000000000..e2766239c --- /dev/null +++ b/tests/test_burger.py @@ -0,0 +1,93 @@ +"""Тесты для класса Burger""" +import pytest +from unittest.mock import Mock +from tests.test_data import TEST_PRICE_CALCULATIONS + + +class TestBurger: + """Тестовый класс для проверки класса Burger""" + + def test_init_creates_empty_burger(self, empty_burger): + """Тест инициализации пустого бургера - проверяем только конструктор""" + + assert empty_burger.bun is None and empty_burger.ingredients == [] + + def test_set_buns(self, empty_burger, mock_bun): + """Тест установки булочки - проверяем только метод set_buns""" + empty_burger.set_buns(mock_bun) + + assert empty_burger.bun is not None + + def test_add_ingredient(self, burger_with_bun, mock_ingredient_sauce): + """Тест добавления ингредиента - проверяем только метод add_ingredient""" + initial_count = len(burger_with_bun.ingredients) + burger_with_bun.add_ingredient(mock_ingredient_sauce) + + assert len(burger_with_bun.ingredients) == initial_count + 1 + + def test_remove_ingredient_removes_from_list(self, burger_with_ingredients): + """Тест удаления ингредиента - проверяем только метод remove_ingredient""" + initial_count = len(burger_with_ingredients.ingredients) + burger_with_ingredients.remove_ingredient(0) + + assert len(burger_with_ingredients.ingredients) == initial_count - 1 + + def test_move_ingredient_changes_order(self, burger_with_bun): + """Тест перемещения ингредиента - проверяем только метод move_ingredient""" + + ingredient1 = Mock() + ingredient1.get_name.return_value = "Ingredient 1" + + ingredient2 = Mock() + ingredient2.get_name.return_value = "Ingredient 2" + + + burger_with_bun.add_ingredient(ingredient1) + burger_with_bun.add_ingredient(ingredient2) + + + burger_with_bun.move_ingredient(0, 1) + + + ingredient_names = [ing.get_name() for ing in burger_with_bun.ingredients] + assert ingredient_names == ["Ingredient 2", "Ingredient 1"] + + def test_get_price_returns_correct_total(self, burger_with_ingredients): + """Тест расчета цены - проверяем только метод get_price""" + + burger_with_ingredients.bun.get_price.return_value = 100.0 + burger_with_ingredients.ingredients[0].get_price.return_value = 50.0 + burger_with_ingredients.ingredients[1].get_price.return_value = 80.0 + + assert burger_with_ingredients.get_price() == 330.0 + + def test_get_price_without_bun_raises_error(self, empty_burger): + """Тест расчета цены без булочки""" + + with pytest.raises(AttributeError): + empty_burger.get_price() + + def test_get_receipt_returns_formatted_string(self, burger_with_ingredients): + """Тест формирования чека - проверяем только метод get_receipt""" + + burger_with_ingredients.bun.get_name.return_value = "Test Bun" + burger_with_ingredients.ingredients[0].get_type.return_value = "SAUCE" + burger_with_ingredients.ingredients[0].get_name.return_value = "Hot Sauce" + burger_with_ingredients.ingredients[1].get_type.return_value = "FILLING" + burger_with_ingredients.ingredients[1].get_name.return_value = "Cutlet" + + + from unittest.mock import Mock + burger_with_ingredients.get_price = Mock(return_value=330.0) + + + receipt = burger_with_ingredients.get_receipt() + expected_lines = [ + "(==== Test Bun ====)", + "= sauce Hot Sauce =", + "= filling Cutlet =", + "(==== Test Bun ====)", + "", + "Price: 330.0" + ] + assert receipt == "\n".join(expected_lines) \ No newline at end of file diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 000000000..71418b6a1 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,50 @@ +"""Модуль с тестовыми данными для всех тестов""" + +# Тестовые данные для булочек +TEST_BUNS = [ + {"name": "black bun", "price": 100.0}, + {"name": "white bun", "price": 200.0}, + {"name": "red bun", "price": 300.0}, + {"name": "Special Bun", "price": 150.5}, + {"name": "Test Bun", "price": 100.0}, + {"name": "Another Bun", "price": 0.0}, + {"name": "Expensive Bun", "price": 999.99}, + {"name": "Bun 1", "price": 100.0}, + {"name": "Bun 2", "price": 0.0}, + {"name": "Bun 3", "price": 50.5}, + {"name": "Single Bun", "price": 100.0}, + {"name": "Bun Only", "price": 100.0}, +] + +# Тестовые данные для ингредиентов +TEST_INGREDIENTS = [ + {"type": "SAUCE", "name": "hot sauce", "price": 100.0}, + {"type": "SAUCE", "name": "sour cream", "price": 200.0}, + {"type": "SAUCE", "name": "chili sauce", "price": 300.0}, + {"type": "SAUCE", "name": "Test Sauce", "price": 50.0}, + {"type": "FILLING", "name": "cutlet", "price": 100.0}, + {"type": "FILLING", "name": "dinosaur", "price": 200.0}, + {"type": "FILLING", "name": "sausage", "price": 300.0}, + {"type": "FILLING", "name": "Test Filling", "price": 80.0}, + {"type": "SAUCE", "name": "Single Sauce", "price": 50.0}, +] + +# Тестовые данные для проверки цены +TEST_PRICE_CALCULATIONS = [ + {"bun_price": 100.0, "sauce_price": 50.0, "filling_price": 80.0, "expected": 330.0}, + {"bun_price": 0.0, "sauce_price": 0.0, "filling_price": 0.0, "expected": 0.0}, + {"bun_price": 50.5, "sauce_price": 25.25, "filling_price": 75.75, "expected": 202.0}, +] + +# Тестовые данные для перемещения ингредиентов +TEST_MOVE_INGREDIENT = [ + {"from_index": 0, "to_index": 1, "expected_order": [1, 0]}, + {"from_index": 1, "to_index": 0, "expected_order": [1, 0]}, + {"from_index": 0, "to_index": 0, "expected_order": [0, 1]}, +] + +# Тестовые данные для удаления ингредиентов +TEST_REMOVE_INGREDIENT = [ + {"index_to_remove": 0, "expected_count": 1}, + {"index_to_remove": 1, "expected_count": 1}, +] diff --git a/tests/test_database.py b/tests/test_database.py new file mode 100644 index 000000000..675cb65ec --- /dev/null +++ b/tests/test_database.py @@ -0,0 +1,46 @@ +"""Тесты для класса Database""" +import pytest +from tests.test_data import TEST_BUNS, TEST_INGREDIENTS + + +class TestDatabase: + """Тестовый класс для проверки класса Database""" + + def test_available_buns_returns_all_buns(self, database): + """Тест метода available_buns - проверяем только этот метод""" + buns = database.available_buns() + + assert len(buns) == 3 + + def test_available_ingredients_returns_all_ingredients(self, database): + """Тест метода available_ingredients - проверяем только этот метод""" + ingredients = database.available_ingredients() + + assert len(ingredients) == 6 + + def test_available_buns_returns_bun_objects(self, database): + """Тест что available_buns возвращает объекты Bun""" + from praktikum.bun import Bun + buns = database.available_buns() + + assert all(isinstance(bun, Bun) for bun in buns) + + def test_available_ingredients_returns_ingredient_objects(self, database): + """Тест что available_ingredients возвращает объекты Ingredient""" + from praktikum.ingredient import Ingredient + ingredients = database.available_ingredients() + + assert all(isinstance(ing, Ingredient) for ing in ingredients) + + def test_buns_have_correct_data(self, database): + """Тест данных булочек - прямой доступ к атрибуту buns""" + + bun_names = [bun.get_name() for bun in database.buns] + assert bun_names == ["black bun", "white bun", "red bun"] + + def test_ingredients_have_correct_types(self, database): + """Тест типов ингредиентов - прямой доступ к атрибуту ingredients""" + + ingredient_types = [ing.get_type() for ing in database.ingredients] + + assert "SAUCE" in ingredient_types and "FILLING" in ingredient_types \ No newline at end of file diff --git a/tests/test_ingredient.py b/tests/test_ingredient.py new file mode 100644 index 000000000..4cc365f8b --- /dev/null +++ b/tests/test_ingredient.py @@ -0,0 +1,56 @@ +"""Тесты для класса Ingredient""" +import pytest +from praktikum.ingredient import Ingredient +from tests.test_data import TEST_INGREDIENTS + + +class TestIngredient: + """Тестовый класс для проверки класса Ingredient""" + + @pytest.mark.parametrize("test_data", [ + TEST_INGREDIENTS[0], # SAUCE, hot sauce, 100 + TEST_INGREDIENTS[1], # SAUCE, sour cream, 200 + TEST_INGREDIENTS[4], # FILLING, cutlet, 100 + TEST_INGREDIENTS[5], # FILLING, dinosaur, 200 + ]) + def test_ingredient_initialization(self, test_data): + """Тест инициализации ингредиента с параметризацией""" + + ingredient = Ingredient(test_data["type"], test_data["name"], test_data["price"]) + + + assert ingredient.type == test_data["type"] + assert ingredient.name == test_data["name"] + assert ingredient.price == test_data["price"] + + @pytest.mark.parametrize("price", [100.0, 0.0, 50.5, 999.99]) + def test_get_price_returns_correct_value(self, price): + """Тест метода get_price с параметризацией""" + + ingredient = Ingredient("SAUCE", "test", price) + + + assert ingredient.get_price() == price + + @pytest.mark.parametrize("test_data", [ + TEST_INGREDIENTS[0], # hot sauce + TEST_INGREDIENTS[1], # sour cream + TEST_INGREDIENTS[4], # cutlet + TEST_INGREDIENTS[5], # dinosaur + ]) + def test_get_name_returns_correct_value(self, test_data): + """Тест метода get_name с параметризацией""" + + ingredient = Ingredient(test_data["type"], test_data["name"], 100) + + + assert ingredient.get_name() == test_data["name"] + + @pytest.mark.parametrize("ingredient_type", ["SAUCE", "FILLING"]) + def test_get_type_returns_correct_value(self, ingredient_type): + """Тест метода get_type с параметризацией""" + + ingredient = Ingredient(ingredient_type, "test", 100) + + + assert ingredient.get_type() == ingredient_type diff --git a/tests/test_praktikum.py b/tests/test_praktikum.py new file mode 100644 index 000000000..17ac4adf7 --- /dev/null +++ b/tests/test_praktikum.py @@ -0,0 +1,18 @@ +"""Тесты для основного модуля praktikum""" +import pytest +import sys +from io import StringIO + +def test_main_executes_without_error(): + """Тест что main выполняется без ошибок""" + from praktikum.praktikum import main + main() + +def test_main_has_output(capsys): + """Тест что main выводит результат""" + from praktikum.praktikum import main + + main() + captured = capsys.readouterr() + + assert captured.out != "" \ No newline at end of file