История 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.comparam2 = admin123param3 = 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