From e718bf48d432ead28082ee9e706cec77c9e83beb Mon Sep 17 00:00:00 2001 From: VolandSZ <{E-MAIL}> Date: Mon, 31 Aug 2026 17:37:21 +0300 Subject: [PATCH] refactor: hierarchical test architecture (Project > Role > BP > Steps) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реструктурированы тесты под иерархию Проект > Личный кабинет > БП > Шаги. Архитектура: - Бизнес-процесс = 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 --- .gitignore | 1 - pytest.ini | 3 +- requirements.txt | 1 + tests/01_vetdept/001_login_test.py | 53 ----------- tests/01_vetdept/BP01_navigation/__init__.py | 1 + .../BP01_navigation/test_bp01_navigation.py | 56 ++++++++++++ tests/01_vetdept/conftest.py | 27 ++++++ tests/conftest.py | 87 ++++++++++++++----- 8 files changed, 153 insertions(+), 76 deletions(-) delete mode 100644 tests/01_vetdept/001_login_test.py create mode 100644 tests/01_vetdept/BP01_navigation/__init__.py create mode 100644 tests/01_vetdept/BP01_navigation/test_bp01_navigation.py create mode 100644 tests/01_vetdept/conftest.py diff --git a/.gitignore b/.gitignore index 788bfee..f16dbfd 100644 --- a/.gitignore +++ b/.gitignore @@ -176,4 +176,3 @@ cython_debug/ # Test reports html-report/ -report.html diff --git a/pytest.ini b/pytest.ini index 76a5ccf..dc1a862 100644 --- a/pytest.ini +++ b/pytest.ini @@ -6,4 +6,5 @@ markers = vetstation: тесты для роли Ветеринарная станция prefecture: тесты для роли Администрация contractor_admin: тесты для роли Администратор организации отлова - smoke: критически важные тесты для быстрого прогона \ No newline at end of file + smoke: критически важные тесты для быстрого прогона + dependency: зависимость шага от предыдущего (pytest-dependency) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9088fc9..9275782 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ pytest pytest-playwright pytest-xdist pytest-html +pytest-dependency python-dotenv \ No newline at end of file diff --git a/tests/01_vetdept/001_login_test.py b/tests/01_vetdept/001_login_test.py deleted file mode 100644 index eeddb47..0000000 --- a/tests/01_vetdept/001_login_test.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/tests/01_vetdept/BP01_navigation/__init__.py b/tests/01_vetdept/BP01_navigation/__init__.py new file mode 100644 index 0000000..ffcbe91 --- /dev/null +++ b/tests/01_vetdept/BP01_navigation/__init__.py @@ -0,0 +1 @@ +# BP01: Навигация по разделам портала diff --git a/tests/01_vetdept/BP01_navigation/test_bp01_navigation.py b/tests/01_vetdept/BP01_navigation/test_bp01_navigation.py new file mode 100644 index 0000000..44fb47d --- /dev/null +++ b/tests/01_vetdept/BP01_navigation/test_bp01_navigation.py @@ -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 + ) diff --git a/tests/01_vetdept/conftest.py b/tests/01_vetdept/conftest.py new file mode 100644 index 0000000..26a40d3 --- /dev/null +++ b/tests/01_vetdept/conftest.py @@ -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 {} diff --git a/tests/conftest.py b/tests/conftest.py index 725f3eb..ba85b41 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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() \ No newline at end of file