refactor: hierarchical test architecture (Project > Role > BP > Steps)
Реструктурированы тесты под иерархию Проект > Личный кабинет > БП > Шаги. Архитектура: - Бизнес-процесс = Python-класс, шаг = метод класса - Шаги именуются test_s01_*, test_s02_* и т.д. - БП именуются BP01_, BP02_ и т.д., каждый в своей папке Передача данных: - browser_page (scope=class) — один браузер на весь БП, авторизация 1 раз - bp_state (scope=class) — словарь для передачи данных между шагами одного БП - shared_state (scope=session) — словарь для передачи между разными БП Зависимости шагов (pytest-dependency): - Если шаг N упал, шаги N+1... получают статус SKIPPED автоматически - Имена зависимостей: BP01::s01, BP01::s02 и т.д. Изменения файлов: - tests/conftest.py: добавлены browser_page и bp_state, вынесены вспомогательные функции _get_role и _login, сохранена role_page (deprecated) для обратной совместимости - tests/01_vetdept/conftest.py: новый, содержит shared_state - tests/01_vetdept/BP01_navigation/: новая папка с тестом навигации - tests/01_vetdept/001_login_test.py: удалён (переписан в BP01) - requirements.txt: добавлен pytest-dependency - pytest.ini: добавлен маркер dependency
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -176,4 +176,3 @@ cython_debug/
|
||||
|
||||
# Test reports
|
||||
html-report/
|
||||
report.html
|
||||
|
||||
@ -6,4 +6,5 @@ markers =
|
||||
vetstation: тесты для роли Ветеринарная станция
|
||||
prefecture: тесты для роли Администрация
|
||||
contractor_admin: тесты для роли Администратор организации отлова
|
||||
smoke: критически важные тесты для быстрого прогона
|
||||
smoke: критически важные тесты для быстрого прогона
|
||||
dependency: зависимость шага от предыдущего (pytest-dependency)
|
||||
@ -3,4 +3,5 @@ pytest
|
||||
pytest-playwright
|
||||
pytest-xdist
|
||||
pytest-html
|
||||
pytest-dependency
|
||||
python-dotenv
|
||||
@ -1,53 +0,0 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
@pytest.mark.vetdept
|
||||
def test_dashboard(role_page: Page):
|
||||
print("\n[TEST] ТЕСТ: Проверка навигации по разделам портала")
|
||||
print("=" * 60)
|
||||
|
||||
# Шаг 1
|
||||
print("\n[1/6] Переход на главную страницу (Dashboard)...")
|
||||
role_page.goto("https://vet.goznak.ru/dashboard")
|
||||
print(" [OK] Страница загружена")
|
||||
|
||||
# Шаг 2
|
||||
print("[2/6] Проверка URL главной страницы...")
|
||||
expect(role_page).to_have_url("https://vet.goznak.ru/dashboard")
|
||||
print(" [OK] URL соответствует ожидаемому")
|
||||
|
||||
# Шаг 3
|
||||
print("[3/6] Проверка заголовка организации...")
|
||||
expect(role_page.locator("#app")).to_contain_text("Управление Ветеринарии Гознак")
|
||||
print(" [OK] Заголовок 'Управление Ветеринарии Гознак' найден")
|
||||
|
||||
# Шаг 4
|
||||
print("[4/6] Переход в раздел 'Организации'...")
|
||||
role_page.get_by_role("link", name="Организации").click()
|
||||
print(" [OK] Клик выполнен, ожидание перехода...")
|
||||
|
||||
print(" Проверка URL раздела 'Организации'...")
|
||||
expect(role_page).to_have_url("https://vet.goznak.ru/organizations", timeout=10000)
|
||||
print(" [OK] URL раздела верный")
|
||||
|
||||
print(" Проверка заголовка страницы...")
|
||||
expect(role_page.get_by_text("Подведомственные организации")).to_be_visible(timeout=10000)
|
||||
print(" [OK] Заголовок 'Подведомственные организации' найден")
|
||||
|
||||
# Шаг 5
|
||||
print("[5/6] Переход в раздел 'Заявки на отлов'...")
|
||||
role_page.get_by_role("link", name="Заявки на отлов").click()
|
||||
print(" [OK] Клик выполнен, ожидание перехода...")
|
||||
|
||||
# Шаг 6
|
||||
print("[6/6] Проверка страницы 'Заявки на отлов'...")
|
||||
expect(role_page).to_have_url("https://vet.goznak.ru/claims-list", timeout=10000)
|
||||
print(" [OK] URL раздела верный")
|
||||
|
||||
expect(role_page.get_by_text("Мои заявки")).to_be_visible(timeout=10000)
|
||||
print(" [OK] Заголовок 'Мои заявки' найден")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🎉 ВСЕ ПРОВЕРКИ ПРОЙДЕНУ УСПЕШНО!")
|
||||
print("=" * 60)
|
||||
1
tests/01_vetdept/BP01_navigation/__init__.py
Normal file
1
tests/01_vetdept/BP01_navigation/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# BP01: Навигация по разделам портала
|
||||
56
tests/01_vetdept/BP01_navigation/test_bp01_navigation.py
Normal file
56
tests/01_vetdept/BP01_navigation/test_bp01_navigation.py
Normal file
@ -0,0 +1,56 @@
|
||||
import os
|
||||
import pytest
|
||||
# pyrefly: ignore [missing-import]
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
|
||||
|
||||
|
||||
@pytest.mark.vetdept
|
||||
@pytest.mark.usefixtures("browser_page")
|
||||
class TestBP01Navigation:
|
||||
"""
|
||||
BP01: Навигация по разделам портала
|
||||
|
||||
Проверяет что пользователь с ролью 'Управление Ветеринарии' может:
|
||||
- Открыть главную страницу (Dashboard)
|
||||
- Видеть заголовок своей организации
|
||||
- Перейти в раздел Организации
|
||||
- Перейти в раздел Заявки на отлов
|
||||
"""
|
||||
|
||||
@pytest.mark.dependency(name="BP01::s01")
|
||||
def test_s01_open_dashboard(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 1: Открыть главную страницу (Dashboard)"""
|
||||
browser_page.goto(BASE_URL + "/dashboard")
|
||||
expect(browser_page).to_have_url(BASE_URL + "/dashboard")
|
||||
bp_state["start_url"] = browser_page.url
|
||||
|
||||
@pytest.mark.dependency(name="BP01::s02", depends=["BP01::s01"])
|
||||
def test_s02_check_org_header(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 2: Проверить заголовок организации в интерфейсе"""
|
||||
expect(browser_page.locator("#app")).to_contain_text(
|
||||
"Управление Ветеринарии Гознак"
|
||||
)
|
||||
|
||||
@pytest.mark.dependency(name="BP01::s03", depends=["BP01::s02"])
|
||||
def test_s03_go_to_organizations(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 3: Перейти в раздел Организации"""
|
||||
browser_page.get_by_role("link", name="Организации").click()
|
||||
expect(browser_page).to_have_url(
|
||||
BASE_URL + "/organizations", timeout=10000
|
||||
)
|
||||
expect(
|
||||
browser_page.get_by_text("Подведомственные организации")
|
||||
).to_be_visible(timeout=10000)
|
||||
|
||||
@pytest.mark.dependency(name="BP01::s04", depends=["BP01::s03"])
|
||||
def test_s04_go_to_claims(self, browser_page: Page, bp_state: dict):
|
||||
"""Шаг 4: Перейти в раздел Заявки на отлов"""
|
||||
browser_page.get_by_role("link", name="Заявки на отлов").click()
|
||||
expect(browser_page).to_have_url(
|
||||
BASE_URL + "/claims-list", timeout=10000
|
||||
)
|
||||
expect(browser_page.get_by_text("Мои заявки")).to_be_visible(
|
||||
timeout=10000
|
||||
)
|
||||
27
tests/01_vetdept/conftest.py
Normal file
27
tests/01_vetdept/conftest.py
Normal file
@ -0,0 +1,27 @@
|
||||
import pytest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Контейнер для передачи данных между разными БП одной роли (scope=session)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def shared_state() -> dict:
|
||||
"""
|
||||
Словарь для передачи данных между разными бизнес-процессами
|
||||
в рамках одной тестовой сессии.
|
||||
|
||||
Используй когда результат одного БП нужен в другом.
|
||||
Например: BP01 создал заявку → BP02 работает с ней.
|
||||
|
||||
Пример:
|
||||
# В BP01:
|
||||
def test_s03_create(self, browser_page, bp_state, shared_state):
|
||||
bp_state["claim_id"] = "12345"
|
||||
shared_state["claim_id"] = "12345" # передаём в BP02
|
||||
|
||||
# В BP02:
|
||||
def test_s01_open(self, browser_page, shared_state):
|
||||
claim_id = shared_state["claim_id"]
|
||||
"""
|
||||
return {}
|
||||
@ -1,6 +1,9 @@
|
||||
import os
|
||||
# pyrefly: ignore [missing-import]
|
||||
import pytest
|
||||
# pyrefly: ignore [missing-import]
|
||||
from dotenv import load_dotenv
|
||||
# pyrefly: ignore [missing-import]
|
||||
from playwright.sync_api import Page
|
||||
|
||||
# Загружаем переменные из .env (файл находится в корне проекта)
|
||||
@ -9,7 +12,7 @@ load_dotenv()
|
||||
# Базовый URL портала — берётся из .env или дефолт
|
||||
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
|
||||
|
||||
# Словарь с учетными данными для разных ролей — берутся из .env
|
||||
# Учётные данные для разных ролей — берутся из .env
|
||||
CREDENTIALS = {
|
||||
"vetdept": {
|
||||
"login": os.getenv("VETDEPT_LOGIN"),
|
||||
@ -26,41 +29,83 @@ CREDENTIALS = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def role_page(page: Page, request):
|
||||
"""
|
||||
Фикстура, которая определяет роль теста и автоматически авторизует пользователя.
|
||||
Учётные данные загружаются из файла .env (не хранятся в коде).
|
||||
"""
|
||||
role = None
|
||||
|
||||
def _get_role(request) -> str:
|
||||
"""Определяет роль теста по pytest-маркеру."""
|
||||
for marker in request.node.iter_markers():
|
||||
if marker.name in CREDENTIALS:
|
||||
role = marker.name
|
||||
break
|
||||
return marker.name
|
||||
pytest.fail("ОШИБКА: У теста нет маркера роли (например, @pytest.mark.vetdept)!")
|
||||
|
||||
# Если маркер не найден, честно говорим об этом (вместо молчаливого NameError)
|
||||
if role is None:
|
||||
pytest.fail("ОШИБКА: У теста нет маркера роли (например, @pytest.mark.vetstation)!")
|
||||
|
||||
def _login(page: Page, role: str) -> None:
|
||||
"""Выполняет авторизацию на портале под указанной ролью."""
|
||||
creds = CREDENTIALS[role]
|
||||
|
||||
# Проверяем что .env заполнен
|
||||
if not creds["login"] or not creds["password"]:
|
||||
pytest.fail(
|
||||
f"ОШИБКА: Переменные окружения для роли '{role}' не заданы. "
|
||||
f"Скопируй .env.example в .env и заполни данные."
|
||||
)
|
||||
|
||||
print(f"\n[АВТОЛОГИН] Выполняется вход под ролью: {role}")
|
||||
|
||||
print(f"\n[АВТОЛОГИН] Вход под ролью: {role}")
|
||||
page.goto(BASE_URL)
|
||||
page.get_by_role("textbox", name="Email").fill(creds["login"])
|
||||
page.get_by_role("textbox", name="Введите пароль").fill(creds["password"])
|
||||
page.get_by_role("button", name="Войти в аккаунт").click()
|
||||
|
||||
page.wait_for_selector("#app", timeout=10000)
|
||||
|
||||
yield page
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Фикстура для бизнес-процессов (scope=class)
|
||||
# Один браузер на весь класс — авторизация выполняется один раз.
|
||||
# Создаёт собственный context и page, чтобы избежать ScopeMismatch
|
||||
# с function-scoped page фикстурой pytest-playwright.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def browser_page(browser, request):
|
||||
"""
|
||||
Авторизованная страница с жизненным циклом на весь класс (бизнес-процесс).
|
||||
Все шаги внутри класса работают в одной вкладке браузера.
|
||||
"""
|
||||
role = _get_role(request)
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
_login(page, role)
|
||||
yield page
|
||||
context.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Контейнер для передачи данных между шагами одного БП (scope=class)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def bp_state() -> dict:
|
||||
"""
|
||||
Словарь для передачи данных между шагами одного бизнес-процесса.
|
||||
Создаётся при старте класса, уничтожается после его завершения.
|
||||
|
||||
Пример:
|
||||
def test_s01_create(self, browser_page, bp_state):
|
||||
bp_state["claim_id"] = "12345"
|
||||
|
||||
def test_s02_verify(self, browser_page, bp_state):
|
||||
assert bp_state["claim_id"] == "12345"
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Устаревшая фикстура — оставлена для обратной совместимости
|
||||
# Используй browser_page для новых тестов
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def role_page(page: Page, request):
|
||||
"""
|
||||
[DEPRECATED] Авторизованная страница с жизненным циклом на один тест.
|
||||
Для новых тестов используй browser_page (scope=class).
|
||||
"""
|
||||
role = _get_role(request)
|
||||
_login(page, role)
|
||||
yield page
|
||||
page.context.clear_cookies()
|
||||
Reference in New Issue
Block a user