Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__pycache__/
.pytest_cache/
.venv/
.coverage
htmlcov/
*.pyc
allure-results/
allure-report/
56 changes: 48 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,64 @@
## Задание 1: Юнит-тесты
# Дипломный проект: Stellar Burgers

## Задание 1: юнит-тесты

### Автотесты для проверки программы, которая помогает заказать бургер в Stellar Burgers

### Реализованные сценарии

Созданы юнит-тесты, покрывающие классы `Bun`, `Burger`, `Ingredient`, `Database`
Класс `Burger` покрыт unit-тестами с использованием моков и параметризации:

- создание бургера;
- установка булочки;
- добавление и удаление ингредиента;
- перемещение ингредиента;
- расчёт цены;
- формирование чека.

Процент покрытия 100% (отчет: `htmlcov/index.html`)
Покрытие файла `burger.py` — 100%.

### Структура проекта

- `praktikum` - пакет, содержащий код программы
- `tests` - пакет, содержащий тесты, разделенные по классам. Например, `bun_test.py`, `burger_test.py` и т.д.
- `burger.py` — тестируемый класс `Burger`;
- `bun.py`, `ingredient.py` — модели, используемые классом `Burger`;
- `tests/test_burger.py` — unit-тесты;
- `tests/api/` — API-тесты;
- `api_client.py` — клиент для запросов к API;
- `constants.py` — адреса эндпоинтов и ожидаемые сообщения API;
- `data.py` — генерация уникальных тестовых пользователей;
- `requirements.txt` — зависимости проекта;
- `pytest.ini` — настройки pytest.

### Запуск автотестов

**Установка зависимостей**

> `$ pip install -r requirements.txt`
> `$ python -m pip install -r requirements.txt`

**Запуск автотестов с проверкой обязательного покрытия 100%**

> `$ pytest tests --cov=burger --cov-report=term-missing --cov-fail-under=100`

**Создание HTML-отчёта**

> `$ pytest tests --cov=burger --cov-report=html --cov-fail-under=100`

## Задание 2: API-тесты

Проверены эндпоинты создания пользователя, авторизации и создания заказа.
Для обязательных полей и неверных учётных данных используется параметризация.
Созданные тестами пользователи удаляются после выполнения проверок.

**Запуск API-тестов с формированием данных Allure**

> `$ pytest tests/api --alluredir=allure-results`

**Просмотр отчёта**

Если Allure Commandline установлен:

**Запуск автотестов и создание HTML-отчета о покрытии**
> `$ allure serve allure-results`

> `$ pytest --cov=praktikum --cov-report=html`
По документации заказ без авторизации должен возвращать `401 Unauthorized`.
Если стенд возвращает `200 OK`, соответствующий тест падает и фиксирует дефект API;
ожидаемый результат не заменяется фактическим поведением неисправного стенда.
69 changes: 69 additions & 0 deletions api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import allure
import requests

from constants import (
BASE_URL,
INGREDIENTS_ENDPOINT,
LOGIN_ENDPOINT,
ORDERS_ENDPOINT,
REGISTER_ENDPOINT,
REQUEST_TIMEOUT,
USER_ENDPOINT,
)


class StellarBurgersApi:
def __init__(self):
self.session = requests.Session()

def close(self):
self.session.close()

@staticmethod
def _attach_response(response):
allure.attach(
f"HTTP {response.status_code}\n{response.text}",
name="Ответ API",
attachment_type=allure.attachment_type.TEXT,
)

def _request(self, method, endpoint, **kwargs):
response = self.session.request(
method,
f"{BASE_URL}{endpoint}",
timeout=REQUEST_TIMEOUT,
**kwargs,
)
self._attach_response(response)
return response

@allure.step("Создать пользователя")
def create_user(self, payload):
return self._request("POST", REGISTER_ENDPOINT, json=payload)

@allure.step("Авторизовать пользователя")
def login_user(self, payload):
return self._request("POST", LOGIN_ENDPOINT, json=payload)

@allure.step("Удалить тестового пользователя")
def delete_user(self, access_token):
return self._request(
"DELETE",
USER_ENDPOINT,
headers={"Authorization": access_token},
)

@allure.step("Получить список ингредиентов")
def get_ingredients(self):
return self._request("GET", INGREDIENTS_ENDPOINT)

@allure.step("Создать заказ")
def create_order(self, ingredient_ids, access_token=None):
headers = {"Authorization": access_token} if access_token else None
return self._request(
"POST",
ORDERS_ENDPOINT,
json={"ingredients": ingredient_ids},
headers=headers,
)

4 changes: 2 additions & 2 deletions burger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import List

from praktikum.bun import Bun
from praktikum.ingredient import Ingredient
from bun import Bun
from ingredient import Ingredient


class Burger:
Expand Down
17 changes: 17 additions & 0 deletions constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
BASE_URL = "https://stellarburgers.education-services.ru"

REGISTER_ENDPOINT = "/api/auth/register"
LOGIN_ENDPOINT = "/api/auth/login"
USER_ENDPOINT = "/api/auth/user"
INGREDIENTS_ENDPOINT = "/api/ingredients"
ORDERS_ENDPOINT = "/api/orders"

REQUEST_TIMEOUT = 30

# Это точные значения поля message в ответах API, поэтому они не переведены.
USER_ALREADY_EXISTS_MESSAGE = "User already exists"
REQUIRED_FIELDS_MESSAGE = "Email, password and name are required fields"
INVALID_CREDENTIALS_MESSAGE = "email or password are incorrect"
INGREDIENTS_REQUIRED_MESSAGE = "Ingredient ids must be provided"
UNAUTHORIZED_MESSAGE = "You should be authorised"

11 changes: 11 additions & 0 deletions data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from uuid import uuid4


def generate_user_data():
unique_part = uuid4().hex
return {
"email": f"qa_baydashev_{unique_part}@yandex.ru",
"password": f"Pwd_{unique_part[:12]}",
"name": "Dmitry",
}

6 changes: 6 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -ra
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pytest==9.1.1
pytest-cov==7.1.0
requests==2.34.2
allure-pytest==2.16.0
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions tests/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

64 changes: 64 additions & 0 deletions tests/api/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import warnings

import pytest
import requests

from api_client import StellarBurgersApi
from data import generate_user_data


@pytest.fixture(scope="session")
def api_client():
client = StellarBurgersApi()
yield client
client.close()


@pytest.fixture
def user_factory(api_client):
created_tokens = []

def create_user(payload=None):
user_data = payload or generate_user_data()
response = api_client.create_user(user_data)

if response.status_code == 200:
token = response.json().get("accessToken")
if token:
created_tokens.append(token)

return user_data, response

yield create_user

for token in created_tokens:
try:
api_client.delete_user(token)
except requests.RequestException as error:
warnings.warn(
f"Не удалось удалить тестового пользователя: {error}",
RuntimeWarning,
)


@pytest.fixture
def registered_user(user_factory):
payload, response = user_factory()
assert response.status_code == 200, response.text

response_body = response.json()
return {
"payload": payload,
"access_token": response_body["accessToken"],
}


@pytest.fixture(scope="session")
def ingredient_ids(api_client):
response = api_client.get_ingredients()
assert response.status_code == 200, response.text

response_body = response.json()
assert response_body["success"] is True
assert len(response_body["data"]) >= 2
return [ingredient["_id"] for ingredient in response_body["data"][:2]]
67 changes: 67 additions & 0 deletions tests/api/test_create_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import allure

from constants import INGREDIENTS_REQUIRED_MESSAGE, UNAUTHORIZED_MESSAGE


@allure.feature("Создание заказа")
class TestCreateOrder:
@allure.title("Авторизованный пользователь может создать заказ")
def test_create_order_with_authorization_success(
self, api_client, registered_user, ingredient_ids
):
response = api_client.create_order(
ingredient_ids[:1], registered_user["access_token"]
)
response_body = response.json()

assert response.status_code == 200
assert response_body["success"] is True
assert isinstance(response_body["order"]["number"], int)

@allure.title("Неавторизованный пользователь не может создать заказ")
def test_create_order_without_authorization_returns_error(
self, api_client, ingredient_ids
):
response = api_client.create_order(ingredient_ids)

assert response.status_code == 401
assert response.json() == {
"success": False,
"message": UNAUTHORIZED_MESSAGE,
}

@allure.title("Можно создать заказ с несколькими ингредиентами")
def test_create_order_with_ingredients_success(
self, api_client, registered_user, ingredient_ids
):
response = api_client.create_order(
ingredient_ids, registered_user["access_token"]
)
response_body = response.json()

assert response.status_code == 200
assert response_body["success"] is True
assert response_body["name"]
assert isinstance(response_body["order"]["number"], int)

@allure.title("Нельзя создать заказ без ингредиентов")
def test_create_order_without_ingredients_returns_error(
self, api_client, registered_user
):
response = api_client.create_order([], registered_user["access_token"])

assert response.status_code == 400
assert response.json() == {
"success": False,
"message": INGREDIENTS_REQUIRED_MESSAGE,
}

@allure.title("Нельзя создать заказ с неверным хешем ингредиента")
def test_create_order_with_invalid_ingredient_hash_returns_error(
self, api_client, registered_user
):
response = api_client.create_order(
["invalid-ingredient-hash"], registered_user["access_token"]
)

assert response.status_code == 500
54 changes: 54 additions & 0 deletions tests/api/test_create_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import allure
import pytest

from constants import REQUIRED_FIELDS_MESSAGE, USER_ALREADY_EXISTS_MESSAGE
from data import generate_user_data


@allure.feature("Создание пользователя")
class TestCreateUser:
@allure.title("Можно создать уникального пользователя")
def test_create_unique_user_success(self, user_factory):
payload, response = user_factory()
response_body = response.json()

assert response.status_code == 200
assert response_body["success"] is True
assert response_body["user"]["email"] == payload["email"]
assert response_body["user"]["name"] == payload["name"]
assert response_body["accessToken"].startswith("Bearer ")
assert response_body["refreshToken"]

@allure.title("Нельзя повторно зарегистрировать существующего пользователя")
def test_create_duplicate_user_returns_error(self, user_factory):
payload, first_response = user_factory()
assert first_response.status_code == 200

second_response = user_factory(payload)[1]

assert second_response.status_code == 403
assert second_response.json() == {
"success": False,
"message": USER_ALREADY_EXISTS_MESSAGE,
}

@allure.title("Нельзя создать пользователя без обязательного поля: {missing_field}")
@pytest.mark.parametrize(
"missing_field",
["email", "password", "name"],
ids=["without_email", "without_password", "without_name"],
)
def test_create_user_without_required_field_returns_error(
self, api_client, missing_field
):
payload = generate_user_data()
payload.pop(missing_field)

response = api_client.create_user(payload)

assert response.status_code == 403
assert response.json() == {
"success": False,
"message": REQUIRED_FIELDS_MESSAGE,
}

Loading