Эта статья — вторая часть
В первой части статьи мы заложили фундамент:
сформулировали мотивацию: зачем нам новый DSL и как он должен выглядеть;
-
провели первые 4 итерации фреймворка scenax, включая:
минимальный
@TestCase()
и поддержку Allure;добавление тегов (
@Feature
,@Severity
,@AllureId
);параметризацию через
@TestCase.each()
;переход к классам (и
runTestClass
,@Suite
,@ParentSuite
,@Layer
);
Теперь — самое интересное.Во второй части увидим:
как строится архитектура
scenax
вокругLifecycle
иStep Library
;как вынести шаги в классы и использовать
@Step
;как фреймворк превращается в полноценного "тестового сценариста";
и почему
Scenax
— это логичное развитие подхода к автотестам.
Первая часть: Scenax — как мы создаём декларативный DSL для автотестов
? Структура статьи
Итерация 5:
@Suite
,@ParentSuite
,@SubSuite
,@Layer
— иерархия тестов в AllureИтерация 6:
@Setup
,@Teardown
,@Context
,@Inject
— жизненный цикл и shared stateИтерация 7:
@BeforeAll
,@AfterAll
,@Setup(params)
— масштабирование сценариевИтерация 8:
@Step
,@Scenario()
и автономные классы шагов](#-итерация-8-step-scenario-и-автономные-классы-шаговКак
scenax
вписывается в стек технологий](#-как-scenax-вписывается-в-стек-технологийЗаключение: почему
scenax
— это новый стандарт для API-тестов на Vitest
Итерация 5: @Suite, @ParentSuite, @SubSuite, @Layer — иерархия тестов в Allure
? Цель
Структурировать тесты как сценарии в документации: по модулям, слоям, группам и уровням ответственности. Добавим иерархические декораторы, чтобы Allure-отчёт стал навигационным.
Почему это важно?
Когда тестов становится много, нужно уметь:
- понимать, какой модуль покрыт
- находить конкретные группы тестов
- запускать только auth
, или smoke
, или e2e
Что добавили
- @ParentSuite(name)
— верхний уровень (напр. E2E Тесты
)
- @Suite(name)
— модуль или раздел (напр. Auth API
)
- @SubSuite(name)
— подгруппа сценариев (напр. Негативные сценарии
)
- @Layer(name)
— технический уровень (api
, unit
, e2e
)
- Поддержка этих декораторов в runTest
- Отображение структуры в Allure
Пример
@ParentSuite('E2E Тесты')
@Suite('Auth API')
@SubSuite('Негативные сценарии')
@Layer('api')
@Feature('Авторизация')
@Tag('regression')
@Tag('auth')
@Owner('team-auth')
class AuthNegativeTests {
@TestCase.each([
['user@example.com', 'wrongpass', 401],
['invalid@example.com', '123456', 401],
])('Логин неуспешен для %s')
@Description('Проверка отказа в доступе при неверных данных')
@Tag('login')
@Severity('critical')
@Owner('dmitry.nkt')
async negativeLogin(email: string, password: string, expectedStatus: number) {
const res = await step(`POST /login с ${email}`, () =>
axios.post('https://httpbin.org/status/' + expectedStatus, { email, password }).catch(e => e.response)
)
expect(res.status).toBe(expectedStatus)
}
}
Как это отображается в Allure
E2E Тесты
└── Auth API
└── Негативные сценарии
└── Логин неуспешен для user@example.com ✅
Что это дает
- ? Навигация по модулям
- ? Группировка по слоям (unit
, api
, e2e
)
- ? Фильтрация по группам (@auth
, @smoke
)
- ? Стандартизация отчётов
- ⚙️ Возможность auto-labeling на CI/CD
Вывод
Теперь каждый тест — это:
- часть конкретной фичи
- вложен в понятную иерархию
- снабжён техническим и смысловым контекстом
Allure-отчёт стал не просто списком проверок, а живым паспортом системы.
Что дальше?
В следующей итерации:
- добавим beforeAll
, afterEach
, глобальные хуки
- начнём поддерживать @Setup
, @Teardown
, возможно — @Inject
и Context
Итерация 6: @Setup, @Teardown, @Context, @Inject — жизненный цикл и shared state
? Цель
Реализовать жизненный цикл для тестов и возможность делиться состоянием между методами и шагами — декларативно и безопасно.
Проблема
Когда тесты становятся сложнее, появляется необходимость:
- подготавливать данные (логин, токен, пользователи)
- очищать ресурсы (удаление, завершение сессии)
- делиться переменными между методами
- логировать внутренние действия
В Vitest это решается beforeEach
/ afterEach
и глобальными переменными — но это не так читаемо как хотелось бы, не типизировано и не структурировано.
Что добавили
- @Setup()
— вызывается перед каждым тестом
- @Teardown()
— вызывается после каждого теста
- @Context()
— создаёт shared-объект для передачи между методами
- @Inject()
— декларативно подставляет значения в поля из контекста- Расширили runTest()
для автоматической поддержки всего этого
- Расширили runTest()
для автоматической поддержки всего этого
Пример
@Feature('Сессия')
@Tag('session')
class SessionTests {
@Context()
ctx!: { token?: string; log?: string[] }
@Inject()
token!: string
@Setup()
async init() {
this.ctx.token = 'admin-token'
this.ctx.log = ['Токен создан']
}
@Teardown()
async cleanup() {
this.ctx.log?.push('Очистка контекста')
attach('Лог выполнения', this.ctx.log?.join('\n') ?? '', 'text/plain')
}
@TestCase('Получение токена')
@Description('Проверка, что токен создаётся и доступен через контекст и инжекцию')
async checkToken() {
await step('Проверка токена из @Context', () => {
expect(this.ctx.token).toBe('admin-token')
})
await step('Проверка токена из @Inject', () => {
this.token = this.ctx.token!
expect(this.token).toBe('admin-token')
})
}
}
Что это даёт
- Изоляцию логики подготовки/очистки
- Стандартизированный shared state
- Возможность вешать логику на @Teardown
— даже вложения в отчёт
- Ясную и декларативную структуру сценариев
А нельзя ли более просто?
Можно. Вот так:
let token: string
beforeEach(() => {
token = 'admin-token'
})
test('тест токена', () => {
expect(token).toBe('admin-token')
})
Работает. Просто. Без магии.
Но когда тестов становится 30+, и каждый — это бизнес-сценарий с шагами, контекстом и вложениями, жизненный цикл превращается в архитектурный элемент — а не в хаотичный beforeEach()
.
Наш подход
- @Setup()
= beforeEach()
с контекстом
- @Teardown()
= afterEach()
+ логика- @Context()
= структурный shared state- @Inject()
= автоматическое внедрение переменных
Вместо “вызови руками” — “объяви намерение”
Killer-фичи DSL подхода (vs обычный vitest)
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()
вызывается без параметров.
@Setup()
— подготовка перед каждым (работает какbeforeEach()
)@Setup(params)
— подготовка с параметрами (работает с.each()
)@BeforeAll()
— общая инициализация (один раз на весь класс)@AfterAll()
— завершение, очистка (один раз после всех)
Что это даёт
- Сокращаем дублирование (login
, createProject
)
- Повышаем читаемость (@Setup([email, status])
)
- Строим классический e2e lifecycle
- Улучшаем Allure-репорты с attach(log)
Следующий шаг
- @Step()
для методов
- Переиспользуемые шаги в стиле Playwright / Serenity
Итерация 8: @Step, @Scenario() и автономные классы шагов
? Цель
Выделить шаги сценария в отдельный класс, сделать их читаемыми, переиспользуемыми и автоматически выполняемыми в Allure-отчётах — без ручного вызова step(...)
и без boilerplate-кода runAllSteps()
.
Решение
1. @Step() — помечает любой метод как шаг для Allure
@Step('Проверка email')
async checkEmail() { ... }
2. @Scenario() — помечает класс, в котором шаги должны выполняться последовательно
@Scenario()
class AccessSteps { ... }
3. runSteps(instance, ctx) — универсальный раннер для таких классов
await runSteps(AccessSteps, this.ctx)
Что мы сделали
- Создали @Step()
— минимальный декларативный шаг
- Добавили @Scenario()
— метку класса для автоматического исполнения
- Написали runSteps()
— запуск шагов по порядку
- Добавили поддержку @Context()
внутри step-класса
Пример использования
@Scenario()
class AccessSteps {
@Context()
ctx!: { email?: string; token?: string; status?: number }
@Step('Проверка email')
async checkEmail() {
expect(this.ctx.email).toMatch(/@example\.com/)
}
@Step('Проверка токена')
async checkToken() {
expect(this.ctx.token).toBe(this.ctx.email + '-token')
}
}
И в тесте:
await runSteps(AccessSteps, this.ctx)
Что это дает?
- ? Выделить шаги в отдельные модули (шаги = lego-блоки сценария)
- ♻️ Использовать один и тот же набор шагов в разных сценариях и классах
- ? Читаемые отчёты Allure с понятными шагами
- ? Автоматическое исполнение — порядок = порядок в коде
- ? Запускать шаги автоматически, без ручного вызова — даже в других раннерах
Это решение вдохновлено практиками Serenity и Playwright, но адаптировано под декларативный DSL.
? Идея на будущее
Можно автоматически вызывать шаги по условию, по профилю, по пользовательской логике. Этот фундамент пригодится нам и для более сложных кейсов: например, вложенных шагов, UI-цепочек, сценариев c branching.
Ключевая мысль
@Step
+@Scenario()
превращают набор методов в декларативный сценарий. (runSteps()
— сценарный раннер)
?️ Как scenax вписывается в стек технологий
Scenax не заменяет Vitest, Playwright или Allure — он добавляет архитектурный слой поверх них, структурируя сценарии, шаги и отчёты:
[ Vitest ] — запускает тесты, проверки
↑
[ allure-vitest ] — интеграция с Allure отчётами
↑
scenax — DSL, шаги, сценарии, архитектура
- Vitest обеспечивает запуск, тайминг, изоляцию тестов.
- Allure даёт визуальный отчёт.
- Scenax структурирует поведение, добавляет шаги, метаинформацию и декларативность.
А что насчёт Playwright, Jest, других?
scenax
построен как архитектурный слой, а не как зависимость от конкретного раннера.
? В будущем планируется:
- @scenax/core
— движок, не завязанный на Vitest
- @scenax/vitest
— адаптер под Vitest
- @scenax/playwright
— адаптер под Playwright
- Возможность автоопределения среды (vitest
, playwright
) через runTest()
Что это даёт?
- Возможность масштабировать архитектуру на любой стек
- Повторное использование сценариев, шагов и логики
- Отвязка от инфраструктурных особенностей раннера
- Потенциал для единой системы тестирования в проекте
В scenax
мы делаем не формат, а подход — который можно применить где угодно, где нужны сценарии, шаги и отчёты.
Заключение: почему scenax — это новый стандарт для API-тестов на Vitest
Восемь итераций. Каждая — шаг к читаемым, структурированным, декларативным тестам, где код становится сценарием, а сценарий — частью продукта.
Изначально мы просто хотели сделать DSL-обёртку над vitest
и allure-js
, чтобы улучшить читаемость и отчётность API-тестов. Но по пути мы пришли к архитектурному паттерну с необходимыми нам фичами, которые:
- давали бы декларативный DSL для API-сценариев на TypeScript
- строили бы тест-кейсы как классы с аннотированными шагами
- автоматически генерировали бы Allure-отчёты без дублирования
- масштабировались бы по BDD-образцу, но без Gherkin.
Так мы пришли к scenax
— на первый взгляд просто обёртке, но при детальном рассмотрении отдельной архитектуре и open-source DSL-фреймворку для построения тестов.
Что даёт scenax прямо сейчас
- ? Читаемые API-тесты, написанные как сценарии
- ? Точная привязка к Allure TestOps: allureId
, severity
, feature
- ? Сценарные шаги (@Step
, @Scenario
, runSteps()
)
- ? Шаги с контекстом, передаваемым автоматически (@Context()
)
- ? Один DSL — один стиль — вся команда пишет одинаково
? «Зачем классы? Все уходят в функции!»
Да, в UI-фреймворках функции вытеснили классы — там нужна реактивность, локальность и гибкость. Но в сценарном тестировании:
- Класс = сценарий (UseCase)
- Метод = шаг
- Декоратор = декларативное описание поведения
- Контекст и расширения интегрируются естественно
? Мы вдохновились Serenity, NestJS и Playwright, но собрали их лучшее — в class-based DSL на TypeScript. Архитектурно, гибко, читаемо.
Что за паттерн мы реализовали?
scenax
реализует архитектуру, которую мы называем Scenario-oriented DSL.
Вдохновлено:
- ? Serenity BDD (Java): идея шагов и сценариев
- ⚙️ NestJS: классы, декораторы, DI
- ? Playwright: изоляция, fixtures, контекст
Но объединено и упрощено для TypeScript-разработки:
- Сценарий = декларативный класс с мета-информацией
- Поведение = управляется методами + контекстом
- Шаги = аннотированные методы, которые легко вызывать
- Расширение = через слои, DI и классы без магии.
Кто должен использовать это прямо сейчас
- Команды, которым нужен читаемый Allure-отчёт, а не набор console.log
- Команды, где есть TestOps или QA, которым нужен живой сценарий
- Архитекторы, которые устали от copy-paste
шагов в тестах
- Разработчики, которым важно писать чисто, предсказуемо и гибко
Есть ли аналоги?
Serenity BDD (Java). Сложный вход, громоздкая структура
Playwright test (TS). Фокус на UI, нет сценариев-классов
vitest
+ allure (TS). Нет DSL, мета-инфо и автоматизации сценариев✅
scenax
(TS). Простой DSL, шаги + сценарии, class-based архитектура
? Потенциал развития
scenax
— это не «утилита». Это библиотека тестовой архитектуры.
В будущем мы планируем:
- ? StepLibrary()
— lego-блоки шагов для переиспользования в сценариях
- ? @Inject()
для nested-инъекций и dependency tree
- ? Интеграция с UI/Playwright тестами на том же DSL
- ? Авто-трекинг статистики шагов, сценариев и покрытия через Allure
- ?️ CLI и VS Code плагины для генерации шаблонов шагов
- ? Архитектурные пресеты для монореп, микросервисов и CI-интеграций
? Заключительная мысль
scenax
— это когда API-тест превращается в намерение,
когда сценарий читается как документация,
и когда команда начинает говорить на одном языке.
Если вы устали от «тестов ради тестов», если вам нужен внятный отчёт, живой DSL и архитектура, которая масштабируется — будем рады вам и вашему вкладу в развитие. Добро пожаловать в scenax
.
Попробуй. Подключи. Покажи команде.
? GitHub: https://github.com/dmitry-nkt/scenax
? Документация: в каждом тесте
? Установи: npm i @scenax/core -D