Skip to main content

История Scenax

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

Что, если бы автотесты читались как сценарий?
Что, если бы каждый шаг был понятен, каждая метка — на месте, а отчёт — пригоден не только для QA, но и для бизнеса?
Так родился Scenax — DSL-фреймворк поверх Vitest и Allure, превращающий тесты в читаемые сценарии.

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

  1. Итерация 1: Минимальный DSL для testCase()
  2. Итерация 2: Метки, allureId, feature, severity
  3. Итерация 3: Параметризация @TestCase.each()
  4. Итерация 4: @Suite, @ParentSuite, @Layer
  5. Итерация 5: Классы и runTest()
  6. Итерация 6: Lifecycle — @BeforeAll, @Setup и @Context
  7. Итерация 7: Управление шагами и Step Library
  8. Итерация 8: @Scenario, @Step, вынос шагов в классы
  9. 🛠️ Как scenax вписывается в стек технологий
  10. 🏁 Заключение: почему scenax — это новый стандарт

🤯 Проблема

Современные инструменты автотестирования мощны — но требуют дисциплины и ручной работы:

  • vitest.test() хорош для unit-проверок, но не подходит для сценариев с шагами и контекстом
  • Метки (feature, severity, tag, owner) задаются вручную, легко ошибиться
  • Шаги через allure.step(...) — не типизированы, не читаемы, не переиспользуемы
  • Нет архитектуры: шаги дублируются, контекст передаётся вручную

В итоге тесты теряют читаемость и перестают быть документацией. Вместо сценария — каша из кода.


💡 Идея

Мы хотим, чтобы тест выглядел как намерение, а не как реализация.

testCase(
'Создание пользователя',
{ id: 'API-001', feature: 'Users', severity: 'critical' },
async () => {
const response = await step('POST /users', () =>
axios.post('/users', { name: 'Иван' })
)
attach('Ответ', response.data, 'application/json')
expect(response.status).toBe(201)
}
)

Это не просто автотест. Это сценарий. Это документ, понятный и разработчику, и аналитику, и бизнесу.


🚀 Что такое Scenax?

Scenax — это:

Scenax — это:

  • DSL (язык сценариев) поверх Vitest + Allure
  • Class-based архитектура: тесты = сценарии, методы = шаги
  • Декораторы: @TestCase, @Feature, @Step, @Context и др.
  • Переиспользуемые шаги и сценарные классы
  • Поддержка параметризации, lifecycle, иерархии
  • Чистые отчёты Allure — без ручного label/step/attach

📚 О чём эта статья

Мы покажем путь — от простого DSL до полноценного архитектурного подхода.
Каждая итерация будет:

  • Давать реальный value (шаг за шагом)
  • Содержать читаемый код
  • Подкрепляться живыми примерами

В финале вы получите:

  • 📦 Переиспользуемую библиотеку scenax
  • 💻 Открытый репозиторий на GitHub
  • ✍️ Готовую статью для команды или Хабра
  • 🔥 И, возможно, новый взгляд на тестирование

Начнём.


Итерация 1: DSL-обёртка testCase, step, attach

🎯 Цель

Сделать API-тест читаемым, логически структурированным и готовым к загрузке в Allure TestOps — без необходимости вручную писать allure.label(...), allure.step(...) и другие низкоуровневые вызовы.


🤯 Проблема

allure-vitest предоставляет доступ к фасаду Allure (метки, шаги, вложения), но при этом API остаётся низкоуровневым и дублируемым:

import * as allure from 'allure-js-commons'

test('profile', async () => {
await allure.label('AS_ID', 'API-123')
await allure.feature('Profile')
await allure.step('GET /profile', async () => {
const res = await axios.get('/profile')
await allure.attachment('response', JSON.stringify(res.data), 'application/json')
expect(res.status).toBe(200)
})
})
  • 🔁 Повторяются одни и те же вызовы
  • ⚠️ Лёгко ошибиться в строковых ключах (AS_ID, severity)
  • 📉 Снижается читаемость и темп разработки

✅ Решение — обёртка

Создаём минимальный DSL: testCase, step, attach
→ единый стиль, чистая структура, меньше ошибок.

testCase(
'Получение профиля',
{ id: 'API-101', feature: 'Профиль', severity: 'critical' },
async () => {
const response = await step('GET /profile', () => axios.get('/profile'))
attach('Ответ', response.data, 'application/json')
expect(response.status).toBe(200)
}
)

Коротко, декларативно, красиво. Всё нужное — на виду.


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

  • ✍️ Написали testCase(name, meta, fn) с поддержкой метаданных
  • 📚 Обернули шаги в step(name, fn)
  • 📎 Добавили attach(name, content) с автосериализацией

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

  • Удаляет boilerplate (label, step, attachment)
  • Повышает читаемость, особенно в отчётах
  • Стандартизирует стиль написания тестов
  • Закладывает фундамент для архитектуры: классы, декораторы, слои

⚙️ Под капотом

import * as allure from 'allure-js-commons'
import { test } from 'vitest'

export function testCase(name, meta, fn) {
test(name, async () => {
if (meta.id) await allure.label('AS_ID', meta.id)
if (meta.feature) await allure.feature(meta.feature)
if (meta.severity) await allure.severity(meta.severity)
await fn()
})
}

export async function step(name, fn) {
return await allure.step(name, fn)
}

export function attach(name, data, type = 'text/plain') {
allure.attachment(name, typeof data === 'string' ? data : JSON.stringify(data), type)
}

📈 Что это даёт на практике

  • ✅ Тест становится читаем как сценарий
  • ✅ Сокращается дублирование
  • ✅ Повышается стандартизация
  • ✅ Уменьшается вероятность ошибок (label('AS_ID', ...) → типизировано)
  • ✅ Повышается качество Allure-отчётов
  • ✅ Готова почва для class-based архитектуры

🧩 Итоги Итерации 1: читаемые тесты, декларативность и язык намерений

Первая итерация дала нам минимально жизнеспособный DSL (testCase, step, attach) для написания API-тестов.
И вроде бы — всего три обёртки. Но они изменили всё.

💡 Что именно мы сделали?

  • Упаковали вызовы фасада в декларативный testCase()
  • Добавили читаемую обёртку step() с логикой прохождения
  • Сделали attach() безопасным, с автосериализацией JSON
  • Добавили поддержку allureId, feature, severity
testCase(
'Получение профиля',
{ id: 'API-101', feature: 'Профиль', severity: 'critical' },
async () => {
const response = await step('GET /profile', () => axios.get(...))
attach('Ответ', response.data, 'application/json')
expect(response.status).toBe(200)
}
)

🧠 Как это назвать?

Мы больше не пишем “тест-функцию”. Мы описываем тест-кейс.

  • Это не unit test, это use-case
  • Это не test(name, () => {}), это testCase(meta, steps)
  • Это не “проверка функции”, это “проверка бизнес-сценария”

📌 Это направление ближе к:

ТерминПодходит?Комментарий
Scenario-based testingесть шаги, сценарии, feature
Use-case testingкаждый testCase — это use-case
Structured test DSLу нас свой язык описания
Classic unit testingслишком низкоуровнево

🔜 Что дальше

В следующей итерации мы превратим тесты в полноценные классы с @TestCase, @Feature, @Severity и единым runTest().

Так мы сделаем ещё один шаг от технической реализации — к тесту как сценарию.


📎 После первой итерации легко подумать:

“Это просто обёртка над vitest и allure, да?”

Но нет. Мы закладываем архитектуру. И это важно.


Scenax — не просто DSL: это архитектура

После первой итерации может показаться: “Ну, сделали обёртку над test() и allure.label(). Что тут особенного?”

Мы тоже так думали. А потом поняли: это не просто синтаксический сахар. Это архитектурный паттерн.


🧠 Что именно мы изобрели?

Мы отделили 3 уровня:

СлойОтвечает за
ScenaxDSL, структура сценария, декларативность
VitestИсполнение, ассерты, test-runner
AllureОтчётность, визуализация, TestOps

Мы не просто вызываем allure.step(). Мы проектируем сценарии, которые могут быть перенесены в любой другой runtime.

Это значит, что:

  • Сценарий можно запустить с другим runner (например, Playwright или Mocha)
  • В будущем можно заменить Allure на другой отчётчик — структура теста сохранится
  • Один и тот же DSL можно использовать как для API, так и для UI тестов

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

Любая большая команда сталкивается с:

  • Расхождением в стиле написания тестов
  • Дублированием шагов (copy-paste)
  • Разными форматами отчётности

Мы предлагаем решение:

  • Один DSL для всей команды
  • Единый слой намерений, независимый от фреймворка
  • Сценарии, которые можно показывать не только QA, но и бизнесу

🔍 Scenax = декларация намерений

testCase('Создание пользователя', async () => {
const res = await step('POST /users', () => api.createUser(...))
attach('Ответ', res.data)
})

Здесь нет привязки к vitest, jest, cypress или playwright. Это просто декларация сценария.


🚀 Что дальше?

В следующей итерации мы начнём превращать сценарии в тест-классы с @TestCase, @Feature, @Severity и общим runTest().

Тесты станут ближе к Cucumber, но без Gherkin. Без шагов Given/When/Then, но с той же логикой: понятный сценарий, описанный декларативно.

➡️ Готовы? Переходим ко второй итерации.

Итерация 2: Классы, декораторы и runTest()

🎯 Цель

Перевести тесты из функций в структурированные классы, чтобы:

  • сократить дублирование шагов
  • повысить читаемость
  • обеспечить масштабируемую архитектуру

🤔 Проблема

В прошлой итерации мы писали тесты в функциональном стиле:

testCase('Получение профиля', async () => {
const res = await step('GET /profile', () => api.getProfile())
attach('Ответ', res.data)
expect(res.status).toBe(200)
})

Это читаемо, но быстро приводит к дублированию логики — особенно если в сценарии много шагов или тестов несколько.

Нам нужно:

  • возможность переиспользовать this.ctx
  • структурировать шаги как методы
  • сгруппировать тесты по классам и feature

✅ Решение — перейти к классам

С помощью декораторов @Feature, @TestCase, @Severity и runTest() мы превращаем каждый тест в метод, а весь сценарий — в класс.

@Feature('Профиль')
class ProfileTest {
@TestCase('Получение профиля', { id: 'API-102', severity: 'critical' })
async testProfile() {
const res = await step('GET /profile', () => api.getProfile())
attach('Ответ', res.data)
expect(res.status).toBe(200)
}
}

runTest(ProfileTest)

🔧 Что сделали

  • Создали декоратор @TestCase(name, meta) для метода
  • Добавили @Feature и @Severity
  • Написали runTest() — адаптер, который превращает все методы в test()
export function runTest(clazz) {
const instance = new clazz()
const proto = Object.getPrototypeOf(instance)

for (const key of Object.getOwnPropertyNames(proto)) {
const method = proto[key]
const meta = Reflect.getMetadata('testcase', instance, key)

if (typeof method === 'function' && meta) {
test(meta.name, async () => {
if (meta.id) await label('AS_ID', meta.id)
if (meta.severity) await severity(meta.severity)
await method.call(instance)
})
}
}
}

📚 Пример в стиле Scenax

@Feature('Профиль')
class ProfileTest {
@TestCase('Проверка имени пользователя', { id: 'API-103', severity: 'normal' })
async checkName() {
const res = await step('GET /profile', () => api.getProfile())
expect(res.data.name).toBe('Иван')
}

@TestCase('Проверка email')
async checkEmail() {
const res = await step('GET /profile', () => api.getProfile())
expect(res.data.email).toMatch(/@example\.com/)
}
}

runTest(ProfileTest)

🧩 Что такое класс в Scenax?

Класс в Scenax — это архитектурная единица, описывающая тестируемый сценарий на уровне бизнес-фичи или контекста.

В терминах проектирования:

  • Это не просто набор методов — это контейнер намерения
  • Он объединяет тест-кейсы по логике, а не по типу
  • Он становится единицей в Allure-отчёте, документации и архитектуре

✳️ Фича или сущность?

Чаще всего — сущность, например: ProfileTest, AuthFlow, PaymentChecks.
Но может быть и логическая группа тестов: RegressionSuite, MobileAPITests, UnauthorizedFlows.


🔧 Что можно повесить на класс?

ДекораторГде применяетсяЧто задаёт
@Feature('Profile')на классНазвание бизнес-фичи
@Suite('API')на классГруппировка в Allure
@ParentSuite('E2E')на классКатегория (UI, e2e, regression и т.п.)
@Layer('e2e')на классАрхитектурный слой
@Context()на полеПередаёт shared state для всех методов
@Inject()на полеВнедряет вспомогательные step-классы

📦 Класс — это:

  • Контейнер тестов с единым контекстом
  • Неймспейс, где можно централизованно задать Feature, Suite, Layer
  • Платформа для Lifecycle-хуков (@BeforeAll, @Setup, @Teardown)
  • Единица документации, которая отображается в Allure как модуль

💡 Класс делает архитектуру тестов явной, предсказуемой и расширяемой

🧠 Почему классы?

  • ✅ Возможность шарить this.ctx, this.client, this.steps
  • ✅ Легко группировать тесты по сущности (@Feature('Profile'))
  • ✅ Можно подключить lifecycle (@BeforeAll, @Setup, @Teardown)
  • ✅ Привычно для backend-разработчиков и архитекторов

Мы не заменяем Vitest. Мы описываем намерения в архитектурной форме.


📈 Что это даёт на практике

БылоСтало
testCase(name, fn)@TestCase() над методом
Метки внутри тела тестаДекораторы над методом/классом
Один тест = одна функцияОдин сценарий = один класс
Нет общего контекстаthis.ctx, this.steps, и др.

🔜 Что дальше

В следующей итерации — сделаем параметризацию тестов через @TestCase.each() и создадим первую полноценную data-driven структуру.

➡️ К одному сценарию — много входов. Много данных. Один стиль.

🔁 Итерация 3: параметризация тестов с @TestCase.each

Один из самых частых паттернов в автотестах — проверка одного сценария с разными данными.
Vitest умеет test.each(...), но наш DSL — тоже.


🧩 Цель

  • Добавить @TestCase.each([...]) — для генерации множественных тестов
  • Автоматически передавать параметры в метод
  • Фиксировать значения в отчёте через allure.parameter

📦 Как выглядит

@Feature('Авторизация')
class AuthTests {
@TestCase.each([
['admin@example.com', 'admin123', 200],
['user@example.com', 'user123', 200],
['hacker@example.com', 'wrongpass', 401],
])('Логин для %s', async (email, password, expectedStatus) => {
const res = await axios.post('/login', { email, password })
expect(res.status).toBe(expectedStatus)
})
}

✅ Что происходит:

  • Генерируются 3 отдельных теста с названиями:
    • Логин для admin@example.com
    • ...
  • Аргументы email, password, expectedStatus передаются в метод
  • Allure фиксирует параметры:
    • param1 = admin@example.com
    • param2 = admin123
    • param3 = 200

🧠 Любое количество аргументов

Метод получает столько аргументов, сколько указано в .each():

@TestCase.each([
['admin', '123', 'desktop', true],
['guest', 'qwerty', 'mobile', false]
])('Попытка входа: %s', async (login, pass, platform, expected) => {
// все аргументы приходят как есть
})

DSL не ограничивает количество параметров — работает как (...args) => {}


📈 Выгода:

  • Читаемость и лаконичность
  • Единая точка теста и данных
  • Allure отображает параметры и шаги для каждого случая
  • Работает с step() и attach()

🎯 Результат

Теперь наш DSL поддерживает один из самых частых паттернов в тестировании.
В следующей итерации — @BeforeEachCase, хуки и re-use шагов.

💭 «Постойте… А чем это лучше обычного test.each

Отличный вопрос.

Вы, скорее всего, сейчас думаете:

«Окей, я вижу @TestCase.each, красиво, декларативно…
Но ведь у Vitest уже есть test.each(...) — разве не то же самое?»

Разберём по полочкам.


🤜 test.each — это удобно. Но…

Когда вы пишете так:

test.each([
['email1', 'pass1', 200],
['email2', 'pass2', 401],
])('Login for %s', ...)

— вы получаете быстрый, минималистичный тест.

Но что если вы хотите:

  • Подсветить фичу (@Feature('Авторизация'))
  • Повесить ID на тест (AS_ID, TMS, Issue)
  • Проставить severity или owner
  • Сделать структурированные шаги внутри отчёта
  • Приложить response JSON в Allure

Всё это вам придётся делать вручную, с кучей allure.label(...), allure.step(...) и allure.attachment(...).


🎯 А вот @TestCase.each — уже про сценарии

@TestCase.each([
['admin@example.com', 'admin123', 200],
['user@example.com', 'user123', 200],
])('Логин для %s', { severity: 'critical' })
async login(email, password, expectedStatus) {
...
}

И вы получаете:

  • ✅ Один метод → несколько кейсов
  • ✅ Метки (severity, feature) — встроены
  • ✅ Название кейса — шаблонное
  • ✅ Параметры отображаются в Allure
  • ✅ Весь отчёт структурирован: шаги, вложения, параметры

📊 Сравнение

Параметрtest.each(...)@TestCase.each(...)
Краткость
Структура отчёта
Метки, severity
Шаблон названия
Поддержка классов
Расширяемость⚠️ Ограничена✅ Управляемая
Интеграция с TestOps
💡 Модель мышленияФункцияТест-кейс

💡 Вывод

Если вы просто хотите повторить тест 3 раза — test.each вас спасёт.
Но если вы описываете бизнес-сценарии, хотите качественные отчёты и масштабируемый DSL,
то @TestCase.each — это уже язык, а не просто удобная функция.
По сути, мы создаём структурированный тестовый фреймворк на базе Vitest, не конкурируя с ним, а надстраивая декларативный слой.

🧩 Итерация 4: описание тестов через @Description, @Tag, @Owner, @Severity

🎯 Цель

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


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

Открываете Allure и видите:
"Логин для admin@example.com" — пройден ✅"

Но больше — ничего. Ни кто владелец, ни зачем тест, ни приоритет.

Всё это важно, особенно когда:

  • 👥 тестов много
  • 📊 нужна аналитика в TestOps
  • 🤝 команда хочет понимать, что тест проверяет

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

Добавили поддержку следующих декораторов:

@Description('Проверяет, что пользователь с валидными данными может авторизоваться')
@Tag('auth')
@Owner('dmitry.nkt')
@Severity('critical')

Теперь они работают как на класс, так и на метод.


🔬 Как это работает

Каждый из этих декораторов:

  • сохраняет значение в metadata через reflect-metadata
  • при запуске в runTest — применяется к Allure через facade (allure.description, allure.tag, ...)

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

@Feature('Авторизация')
@Tag('api')
@Owner('backend-team')
class AuthTests {

@TestCase.each([
['admin@example.com', 'admin123', 200],
['hacker@example.com', 'wrongpass', 401]
])('Логин для %s')
@Description('Проверяет сценарий логина с учётными данными')
@Tag('login')
@Severity('critical')
@Owner('dmitry.nkt')
async login(email, password, expectedStatus) {
const res = await step(`POST /login`, () =>
axios.post('https://httpbin.org/status/' + expectedStatus, { email, password })
)
expect(res.status).toBe(expectedStatus)
}
}

🧠 Что это даёт

  • 📎 Видно, зачем тест (описание)
  • 🧩 Кто его владелец (@Owner)
  • 🚦 Насколько он важен (@Severity)
  • 🏷️ Какой группе принадлежит (@Tag)
  • 📊 Allure и TestOps могут группировать, фильтровать, считать покрытие по owner/feature

🧩 Итог

Мы добавили семантический слой над тестами.
Теперь каждый тест-кейс несёт не только шаги, но и контекст — и для людей, и для систем.


🚧 Что дальше?

В следующей итерации:

  • добавим @Suite, @ParentSuite, @Layer
  • начнём собирать полноценную иерархию тестов
  • подключим auto-labeling для api, regression, smoke

🧱 Итерация 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() для автоматической поддержки всего этого

📦 Пример

@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() вызывается без параметров.


📊 Сравнение с итерацией 6

Жизненный циклДля чегоПоведение
@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 — он добавляет архитектурный слой поверх них, структурируя сценарии, шаги и отчёты:

[ NodeJS / TypeScript runtime ]

[ Vitest ] — запускает тесты, проверки

[ allure-vitest ] — интеграция с Allure отчётами

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

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

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

💡 В будущем планируется:

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

📌 Что это даёт?

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

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

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

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

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

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

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


🧠 Что мы дали вместо "просто тестов"

БылоСтало
test(name, fn)@TestCase() — декларативное описание
await step('...', () => ...)@Step() — переиспользуемые шаги
feature, label, severity вручную@Feature(), @Severity() и т.д.
Тест = одна функцияТест = структура: сценарий + шаги + контекст
Документация отдельноЧитаемый тест = документация

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

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

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

Да, в UI-фреймворках функции вытеснили классы — там нужна реактивность, локальность и гибкость. Но в сценарном тестировании:

  • Класс = сценарий (UseCase)
  • Метод = шаг
  • Декоратор = декларативное описание поведения
  • Контекст и расширения интегрируются естественно

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


📈 Сравнение с Vitest + Allure

ПараметрVitest + Allurescenax
Параметризацияtest.each@TestCase.each() + метаинфо
Структура шагов⚠️ вручную через step()@Step() + runSteps()
Архитектура⚠️ flat-функции✅ сценарии как классы
Контекст❌ руками✅ через @Context()
Стильпроизвольныйединый DSL-стиль
Отчётностьзависит от дисциплинывстроена и стандартизирована

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

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

Вдохновлено:

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

Но объединено и упрощено для TypeScript-разработки:

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

📌 Мы не нашли ни одного аналога, который делал бы всё это в одном DSL.


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

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

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

РешениеЯзыкОтличия
Serenity BDDJavaСложный вход, громоздкая структура
Playwright testTSФокус на UI, нет сценариев-классов
vitest + allureTSНет DSL, мета-инфо и автоматизации сценариев
scenaxTSПростой DSL, шаги + сценарии, class-based архитектура

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

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

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

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

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

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

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

Добро пожаловать в scenax.

🚀 Попробуй. Подключи. Покажи команде — и вы уже не вернётесь назад.


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