История Scenax
Scenax: как превратить API-тесты в читаемые сценарии с Vitest и Allure
Что, если бы автотесты читались как сценарий?
Что, если бы каждый шаг был понятен, каждая метка — на месте, а отчёт — пригоден не только для QA, но и для бизнеса?
Так родился Scenax — DSL-фреймворк поверх Vitest и Allure, превращающий тесты в читаемые сценарии.
📑 Структура статьи
- Итерация 1: Минимальный DSL для
testCase()
- Итерация 2: Метки,
allureId
,feature
,severity
- Итерация 3: Параметризация
@TestCase.each()
- Итерация 4:
@Suite
,@ParentSuite
,@Layer
- Итерация 5: Классы и
runTest()
- Итерация 6: Lifecycle —
@BeforeAll
,@Setup
и@Context
- Итерация 7: Управление шагами и Step Library
- Итерация 8:
@Scenario
,@Step
, вынос шагов в классы - 🛠️ Как
scenax
вписывается в стек технологий - 🏁 Заключение: почему
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 уровня:
Слой | Отвечает за |
---|---|
Scenax | DSL, структура сценария, декларативность |
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
)
- Allure-Ready из коробки (
feature
,severity
,owner
, шаги, лейблы) - Автоматический жизненный цикл —
setup
,teardown
, логирование - Модульная архитектура с иерархией
Suite → SubSuite
- Параметризация сценариев (
@TestCase.each
) - Тест = Документация (
@Description
,@Feature
) - Расширяемость — 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 + Allure | scenax |
---|---|---|
Параметризация | ✅ 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 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
.
🚀 Попробуй. Подключи. Покажи команде — и вы уже не вернётесь назад.
📦 GitHub: https://github.com/dmitry-nkt/scenax
🧪 Документация: в каждом тесте
👉 Установи: npm i -D scenax