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

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

  • сформулировали мотивацию: зачем нам новый 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

Комментарии (0)