Scenax: как превратить API-тесты в читаемые сценарии с Vitest и Allure (Часть 2)

от автора

Эта статья — вторая часть

В первой части статьи мы заложили фундамент:

  • сформулировали мотивацию: зачем нам новый DSL и как он должен выглядеть;

  • провели первые 4 итерации фреймворка scenax, включая:

    • минимальный @TestCase() и поддержку Allure;

    • добавление тегов (@Feature@Severity@AllureId);

    • параметризацию через @TestCase.each();

    • переход к классам (иrunTestClass@Suite@ParentSuite@Layer);

Теперь — самое интересное.Во второй части увидим:

  • как строится архитектура scenax вокруг Lifecycle и Step Library;

  • как вынести шаги в классы и использовать @Step;

  • как фреймворк превращается в полноценного «тестового сценариста»;

  • и почему Scenax — это логичное развитие подхода к автотестам.

Первая часть: Scenax — как мы создаём декларативный DSL для автотестов

📑 Структура статьи

  1. Итерация 5: @Suite, @ParentSuite, @SubSuite, @Layer — иерархия тестов в Allure

  2. Итерация 6: @Setup, @Teardown, @Context, @Inject — жизненный цикл и shared state

  3. Итерация 7: @BeforeAll, @AfterAll, @Setup(params) — масштабирование сценариев

  4. Итерация 8: @Step, @Scenario() и автономные классы шагов](#-итерация-8-step-scenario-и-автономные-классы-шагов

  5. Как scenax вписывается в стек технологий](#-как-scenax-вписывается-в-стек-технологий

  6. Заключение: почему scenax — это новый стандарт для API-тестов на Vitest

Итерация 5: @Suite, @ParentSuite, @SubSuite, @Layer — иерархия тестов в Allure

🎯 Цель

Структурировать тесты как сценарии в документации: по модулям, слоям, группам и уровням ответственности. Добавим иерархические декораторы, чтобы Allure-отчёт стал навигационным.

Почему это важно?

Когда тестов становится много, нужно уметь:

— понимать, какой модуль покрыт
— находить конкретные группы тестов
— запускать только auth, или smoke, или e2e

Что добавили

@ParentSuite(name) — верхний уровень (напр. E2E Тесты)
@Suite(name) — модуль или раздел (напр. Auth API)
@SubSuite(name) — подгруппа сценариев (напр. Негативные сценарии)
@Layer(name) — технический уровень (api, unit, e2e)
— Поддержка этих декораторов в runTest — Отображение структуры в Allure

Пример

@ParentSuite('E2E Тесты') @Suite('Auth API') @SubSuite('Негативные сценарии') @Layer('api') @Feature('Авторизация') @Tag('regression') @Tag('auth') @Owner('team-auth') class AuthNegativeTests {   @TestCase.each([     ['user@example.com', 'wrongpass', 401],     ['invalid@example.com', '123456', 401],   ])('Логин неуспешен для %s')   @Description('Проверка отказа в доступе при неверных данных')   @Tag('login')   @Severity('critical')   @Owner('dmitry.nkt')   async negativeLogin(email: string, password: string, expectedStatus: number) {     const res = await step(`POST /login с ${email}`, () =>       axios.post('https://httpbin.org/status/' + expectedStatus, { email, password }).catch(e => e.response)     )     expect(res.status).toBe(expectedStatus)   } }

Как это отображается в Allure

E2E Тесты └── Auth API     └── Негативные сценарии         └── Логин неуспешен для user@example.com ✅

Что это дает

— 🧭 Навигация по модулям
— 📊 Группировка по слоям (unit, api, e2e)
— 🔍 Фильтрация по группам (@auth, @smoke)
— 💼 Стандартизация отчётов
— ⚙️ Возможность auto-labeling на CI/CD

Вывод

Теперь каждый тест — это:
— часть конкретной фичи
— вложен в понятную иерархию
— снабжён техническим и смысловым контекстом

Allure-отчёт стал не просто списком проверок, а живым паспортом системы.

Что дальше?

В следующей итерации:
— добавим beforeAll, afterEach, глобальные хуки — начнём поддерживать @Setup, @Teardown, возможно — @Inject и Context

Итерация 6: @Setup, @Teardown, @Context, @Inject — жизненный цикл и shared state

🎯 Цель

Реализовать жизненный цикл для тестов и возможность делиться состоянием между методами и шагами — декларативно и безопасно.

Проблема

Когда тесты становятся сложнее, появляется необходимость:

— подготавливать данные (логин, токен, пользователи)
— очищать ресурсы (удаление, завершение сессии)
— делиться переменными между методами
— логировать внутренние действия

В Vitest это решается beforeEach / afterEach и глобальными переменными — но это не так читаемо как хотелось бы, не типизировано и не структурировано.

Что добавили

@Setup() — вызывается перед каждым тестом
@Teardown() — вызывается после каждого теста
@Context() — создаёт shared-объект для передачи между методами
@Inject() — декларативно подставляет значения в поля из контекста- Расширили runTest() для автоматической поддержки всего этого
— Расширили runTest() для автоматической поддержки всего этого

Пример

@Feature('Сессия') @Tag('session') class SessionTests {   @Context()   ctx!: { token?: string; log?: string[] }    @Inject()   token!: string    @Setup()   async init() {     this.ctx.token = 'admin-token'     this.ctx.log = ['Токен создан']   }    @Teardown()   async cleanup() {     this.ctx.log?.push('Очистка контекста')     attach('Лог выполнения', this.ctx.log?.join('\n') ?? '', 'text/plain')   }    @TestCase('Получение токена')   @Description('Проверка, что токен создаётся и доступен через контекст и инжекцию')   async checkToken() {     await step('Проверка токена из @Context', () => {       expect(this.ctx.token).toBe('admin-token')     })      await step('Проверка токена из @Inject', () => {       this.token = this.ctx.token!       expect(this.token).toBe('admin-token')     })   } }

Что это даёт

— Изоляцию логики подготовки/очистки
— Стандартизированный shared state
— Возможность вешать логику на @Teardown — даже вложения в отчёт
— Ясную и декларативную структуру сценариев

А нельзя ли более просто?

Можно. Вот так:

let token: string  beforeEach(() => {   token = 'admin-token' })  test('тест токена', () => {   expect(token).toBe('admin-token') })

Работает. Просто. Без магии.

Но когда тестов становится 30+, и каждый — это бизнес-сценарий с шагами, контекстом и вложениями, жизненный цикл превращается в архитектурный элемент — а не в хаотичный beforeEach().

Наш подход

@Setup() = beforeEach() с контекстом
@Teardown() = afterEach() + логика- @Context() = структурный shared state- @Inject() = автоматическое внедрение переменных

Вместо “вызови руками” — “объяви намерение”

Killer-фичи DSL подхода (vs обычный vitest)

  1. Allure-Ready из коробки (feature, severity, owner, шаги, лейблы)

  2. Автоматический жизненный цикл — setup, teardown, логирование

  3. Модульная архитектура с иерархией Suite → SubSuite

  4. Параметризация сценариев (@TestCase.each)

  5. Тест = Документация (@Description, @Feature)

  6. Расширяемость — auto-labeling, Context, future hooks

Что дальше?

@BeforeAll, @AfterAll — выполнение один раз на класс
@Step — шаги как методы

Итерация 7: @BeforeAll, @AfterAll, @Setup(params) — масштабирование сценариев

🎯 Цель

Добавить поддержку жизненного цикла на уровне класса (@BeforeAll, @AfterAll) и параметризированной подготовки данных (@Setup(params)).

Проблема

Когда у нас появляются десятки сценариев:
— многие требуют авторизации (но не хочется логиниться 10 раз) — каждый сценарий может требовать временных сущностей (юзеры, сессии) — @Setup() не знает, какие параметры передаются через .each()

Что добавили

@BeforeAll() — выполняется один раз до всех тестов класса@AfterAll() — выполняется один раз после всех тестов класса@Setup(params) — теперь получает параметры из @TestCase.each([...])

Пример

@Context() ctx!: { email?: string; status?: number; token?: string; log?: string[] }  @BeforeAll() initSuite() {   this.ctx.log = ['🚀 Начинаем'] }  @AfterAll() finishSuite() {   this.ctx.log?.push('🏁 Конец')   attach('Лог', this.ctx.log?.join('\n') ?? '', 'text/plain') }  @Setup() prepare([email, expectedStatus]) {   this.ctx.email = email   this.ctx.status = expectedStatus   this.ctx.token = email + '-token'   this.ctx.log?.push(`🔧 Подготовка: ${email}`) }

А откуда params в @Setup()?

Если используется @TestCase.each(...), мы передаём параметры прямо в @Setup():

@Setup() prepare([email, status]) { ... } // email и статус приходят из each()

Если используется обычный @TestCase(...)@Setup() вызывается без параметров.

  • @Setup() — подготовка перед каждым (работает как beforeEach())

  • @Setup(params) — подготовка с параметрами (работает с .each())

  • @BeforeAll() — общая инициализация (один раз на весь класс)

  • @AfterAll() — завершение, очистка (один раз после всех)

Что это даёт

— Сокращаем дублирование (login, createProject)
— Повышаем читаемость (@Setup([email, status]))
— Строим классический e2e lifecycle
— Улучшаем Allure-репорты с attach(log)

Следующий шаг

@Step() для методов
— Переиспользуемые шаги в стиле Playwright / Serenity

Итерация 8: @Step, @Scenario() и автономные классы шагов

🎯 Цель

Выделить шаги сценария в отдельный класс, сделать их читаемыми, переиспользуемыми и автоматически выполняемыми в Allure-отчётах — без ручного вызова step(...) и без boilerplate-кода runAllSteps().

Решение

1. @Step() — помечает любой метод как шаг для Allure

@Step('Проверка email') async checkEmail() { ... }

2. @Scenario() — помечает класс, в котором шаги должны выполняться последовательно

@Scenario() class AccessSteps { ... }

3. runSteps(instance, ctx) — универсальный раннер для таких классов

await runSteps(AccessSteps, this.ctx)

Что мы сделали

— Создали @Step() — минимальный декларативный шаг
— Добавили @Scenario() — метку класса для автоматического исполнения
— Написали runSteps() — запуск шагов по порядку
— Добавили поддержку @Context() внутри step-класса

Пример использования

@Scenario() class AccessSteps {   @Context()   ctx!: { email?: string; token?: string; status?: number }    @Step('Проверка email')   async checkEmail() {     expect(this.ctx.email).toMatch(/@example\.com/)   }    @Step('Проверка токена')   async checkToken() {     expect(this.ctx.token).toBe(this.ctx.email + '-token')   } }

И в тесте:

await runSteps(AccessSteps, this.ctx)

Что это дает?

— 🧩 Выделить шаги в отдельные модули (шаги = lego-блоки сценария)
— ♻️ Использовать один и тот же набор шагов в разных сценариях и классах
— 🧠 Читаемые отчёты Allure с понятными шагами
— 🎯 Автоматическое исполнение — порядок = порядок в коде
— 🚀 Запускать шаги автоматически, без ручного вызова — даже в других раннерах

Это решение вдохновлено практиками Serenity и Playwright, но адаптировано под декларативный DSL.

💡 Идея на будущее

Можно автоматически вызывать шаги по условию, по профилю, по пользовательской логике. Этот фундамент пригодится нам и для более сложных кейсов: например, вложенных шагов, UI-цепочек, сценариев c branching.

Ключевая мысль

@Step + @Scenario() превращают набор методов в декларативный сценарий. (runSteps() — сценарный раннер)

🛠️ Как scenax вписывается в стек технологий

Scenax не заменяет Vitest, Playwright или Allure — он добавляет архитектурный слой поверх них, структурируя сценарии, шаги и отчёты:

    [ Vitest ]          — запускает тесты, проверки        ↑ [ allure-vitest ]       — интеграция с Allure отчётами        ↑       scenax            — DSL, шаги, сценарии, архитектура

Vitest обеспечивает запуск, тайминг, изоляцию тестов.
Allure даёт визуальный отчёт.
Scenax структурирует поведение, добавляет шаги, метаинформацию и декларативность.

А что насчёт Playwright, Jest, других?

scenax построен как архитектурный слой, а не как зависимость от конкретного раннера.

💡 В будущем планируется:
@scenax/core — движок, не завязанный на Vitest
@scenax/vitest — адаптер под Vitest
@scenax/playwright — адаптер под Playwright
— Возможность автоопределения среды (vitest, playwright) через runTest()

Что это даёт?

— Возможность масштабировать архитектуру на любой стек
— Повторное использование сценариев, шагов и логики
— Отвязка от инфраструктурных особенностей раннера
— Потенциал для единой системы тестирования в проекте

В scenax мы делаем не формат, а подход — который можно применить где угодно, где нужны сценарии, шаги и отчёты.

Заключение: почему scenax — это новый стандарт для API-тестов на Vitest

Восемь итераций. Каждая — шаг к читаемым, структурированным, декларативным тестам, где код становится сценарием, а сценарий — частью продукта.

Изначально мы просто хотели сделать DSL-обёртку над vitest и allure-js, чтобы улучшить читаемость и отчётность API-тестов. Но по пути мы пришли к архитектурному паттерну с необходимыми нам фичами, которые:

— давали бы декларативный DSL для API-сценариев на TypeScript
— строили бы тест-кейсы как классы с аннотированными шагами
— автоматически генерировали бы Allure-отчёты без дублирования
— масштабировались бы по BDD-образцу, но без Gherkin.

Так мы пришли к scenax — на первый взгляд просто обёртке, но при детальном рассмотрении отдельной архитектуре и open-source DSL-фреймворку для построения тестов.

Что даёт scenax прямо сейчас

— 🧩 Читаемые API-тесты, написанные как сценарии
— 🎯 Точная привязка к Allure TestOps: allureId, severity, feature — 📦 Сценарные шаги (@Step, @Scenario, runSteps())
— 🧠 Шаги с контекстом, передаваемым автоматически (@Context())
— 🧪 Один DSL — один стиль — вся команда пишет одинаково

🤔 «Зачем классы? Все уходят в функции!»

Да, в UI-фреймворках функции вытеснили классы — там нужна реактивность, локальность и гибкость. Но в сценарном тестировании:
— Класс = сценарий (UseCase) — Метод = шаг — Декоратор = декларативное описание поведения — Контекст и расширения интегрируются естественно

📌 Мы вдохновились Serenity, NestJS и Playwright, но собрали их лучшее — в class-based DSL на TypeScript. Архитектурно, гибко, читаемо.

Что за паттерн мы реализовали?

scenax реализует архитектуру, которую мы называем Scenario-oriented DSL.

Вдохновлено:
— 🧠 Serenity BDD (Java): идея шагов и сценариев
— ⚙️ NestJS: классы, декораторы, DI
— 🧪 Playwright: изоляция, fixtures, контекст

Но объединено и упрощено для TypeScript-разработки:
— Сценарий = декларативный класс с мета-информацией
— Поведение = управляется методами + контекстом
— Шаги = аннотированные методы, которые легко вызывать
— Расширение = через слои, DI и классы без магии.

Кто должен использовать это прямо сейчас

— Команды, которым нужен читаемый Allure-отчёт, а не набор console.log — Команды, где есть TestOps или QA, которым нужен живой сценарий
— Архитекторы, которые устали от copy-paste шагов в тестах
— Разработчики, которым важно писать чисто, предсказуемо и гибко

Есть ли аналоги?

  • Serenity BDD (Java). Сложный вход, громоздкая структура

  • Playwright test (TS). Фокус на UI, нет сценариев-классов

  • vitest + allure (TS). Нет DSL, мета-инфо и автоматизации сценариев

  • scenax (TS). Простой DSL, шаги + сценарии, class-based архитектура

🚀 Потенциал развития

scenax — это не «утилита». Это библиотека тестовой архитектуры.

В будущем мы планируем:

— 🧱 StepLibrary() — lego-блоки шагов для переиспользования в сценариях — 🧬 @Inject() для nested-инъекций и dependency tree — 🧠 Интеграция с UI/Playwright тестами на том же DSL — 📊 Авто-трекинг статистики шагов, сценариев и покрытия через Allure — 🛠️ CLI и VS Code плагины для генерации шаблонов шагов — 🧭 Архитектурные пресеты для монореп, микросервисов и CI-интеграций

💬 Заключительная мысль

scenax — это когда API-тест превращается в намерение,
когда сценарий читается как документация,
и когда команда начинает говорить на одном языке.

Если вы устали от «тестов ради тестов», если вам нужен внятный отчёт, живой DSL и архитектура, которая масштабируется — будем рады вам и вашему вкладу в развитие. Добро пожаловать в scenax.

Попробуй. Подключи. Покажи команде.

https://scenax.com/

📦 GitHub: https://github.com/dmitry-nkt/scenax
🧪 Документация: в каждом тесте
👉 Установи: npm i @scenax/core -D


ссылка на оригинал статьи https://habr.com/ru/articles/914604/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *