Authoring Scenarios
How to define testing metadata, scenarios, steps, interpolation, and custom specs in SolidX.
Mental Model
Scenario authoring in SolidX is primarily metadata design, not test scripting.
testing.dataholds reusable fixtures.testing.scenariosdescribes executable flows.saveAsand interpolation connect steps without hard-coding values.test.specexists for the few cases where metadata alone is not expressive enough.
This page explains how to write testing metadata for SolidX.
Where Scenarios Live
Testing definitions live inside a module metadata JSON file under the testing key.
At a high level, the shape looks like this:
{
"testing": {
"specs": ["path/to/register-test-specs.js"],
"roles": [],
"users": [],
"data": [],
"scenarios": []
}
}Top-Level Testing Keys
Quick reference:
scenarios: Executable scenario definitions for API, UI, or mixed testing flows.data: Fixture records loaded before execution and referenced through${data:...}interpolation.users: Test user definitions created duringsolidctl test data --load.roles: Test role definitions with permission bindings.specs: Custom spec registration modules used bytest.spec.
specs
Paths to custom spec registration modules.
Use these when you want to invoke test.spec from a scenario.
roles
Optional role definitions that solidctl test data --load creates in the database.
Each entry names a role and lists the permissions to bind to it:
{
"name": "Editor",
"permissions": [
"BookController.*",
"LoanController.*",
"DashboardController.findMany",
"DashboardController.findOne"
]
}Fields:
name(required): Role name, created if it does not already exist.permissions(optional): List of permission names to bind to this role.
Permission syntax:
- Exact:
ControllerName.methodName - Wildcard:
ControllerName.* - Global:
*
Roles are seeded idempotently: created if absent, left unchanged if they already exist.
Ordering Requirement
Run solidctl seed before solidctl test data --load.
Controller permissions are registered during seeding, and role binding can fail if the required permissions are not already present in the database.
users
Optional user definitions that solidctl test data --load creates in the database.
Each entry provides credentials and an optional list of roles to assign:
{
"username": "libTestEditor",
"email": "libTestEditor@test.local",
"password": "Test@1234",
"fullName": "Library Test Editor",
"roles": ["Editor"]
}Fields:
username(required): Unique username.email(required): Email address.password(required): Login password.fullName(optional): Display name.mobile(optional): Mobile number.roles(optional): List of role names to assign. Declare these intesting.rolesfirst.
Users are skipped if a user with the same username already exists. They are not deleted during teardown.
A typical module defines one user per role category to support access-level scenario coverage:
"users": [
{ "username": "libTestEditor", "email": "libTestEditor@test.local", "password": "Test@1234", "roles": ["Editor"] },
{ "username": "libTestViewer", "email": "libTestViewer@test.local", "password": "Test@1234", "roles": ["Viewer"] },
{ "username": "libTestNoRole", "email": "libTestNoRole@test.local", "password": "Test@1234", "roles": ["NoRole"] }
]data
Test fixture records to load before execution.
Each record typically contains:
modelUserKeyrecUserKeyValuedata
Real project pattern:
- Use
testing.dataas a reusable fixture library. - Express relations through
...UserKeyfields such asstateUserKey,cityUserKey, ortemplateMasterUserKey. - Keep
recUserKeyValuestable so scenarios can reference the fixture by name.
scenarios
The executable scenarios for the module.
This is the core of the testing system.
Scenario Shape
{
"id": "api-authenticate-success",
"name": "Authenticate succeeds",
"type": "api",
"params": {
"username": "alice"
},
"tags": ["smoke"],
"timeoutMs": 30000,
"retries": 1,
"steps": []
}Important Fields
id: Stable scenario identifier.name: Optional human-readable label.type:api,ui, ormixed.params: Free-form scenario parameters.tags: Labels for filtering.timeoutMs: Scenario timeout override.retries: Scenario retry count.steps: The executable flow.
Step Styles
Steps can be written in two ways.
{
"given": { "op": "ui.goto", "with": { "url": "/login" } }
}{
"op": "util.log",
"with": { "message": "Starting scenario" }
}The engine normalizes both forms before execution, so there is no runtime difference between them.
Use given for setup steps, when for the action being tested, then for assertions, and and to continue the previous phase without repeating it.
then also accepts an array, which is useful when you want to group multiple assertions after a single action:
{
"then": [
{ "op": "assert.httpStatus", "with": { "is": 201 } },
{ "op": "assert.jsonPath", "with": { "from": "${res:created}", "path": "$.name", "equals": "Test" } }
]
}Step Fields
Each executable step can include:
op: Required operation name.with: Operation-specific input.saveAs: Save the step result into the resource store.name: Optional reporting label.spec: Custom spec id fortest.spec.timeoutMs: Per-step timeout override.
Interpolation
Before each step runs, the engine resolves interpolation tokens.
Supported token families include:
${env:NAME}for environment variables.${params.foo}for scenario params.${res:path.to.value}for saved runtime resources.${data:modelUserKey["recUserKeyValue"].field}for test data lookups.
Interpolation Rule
Prefer interpolation over hardcoded ids, URLs, and fixture values. It keeps scenarios reusable across isolated test runs.
Examples:
{
"params": {
"state": "${data:stateMaster[\"Maharashtra\"].name}"
}
}{
"when": {
"op": "api.request",
"with": {
"method": "POST",
"url": "${env:API_BASE_URL}/api/example",
"json": {
"stateName": "${params.state}",
"city": "${data:cityMaster[\"New Delhi\"].name}"
}
}
}
}Referencing Test Data
Test data is indexed as:
data:<modelUserKey>["<recUserKeyValue>"]Useful patterns:
.fieldNameto access a single field.._recto access the whole underlying object.
Example:
"${data:cityMaster[\"New Delhi\"]._rec}"The venue module uses this pattern heavily:
- Master fixtures such as
stateMaster["Maharashtra"]. - Relation-aware fixtures such as
cityMaster["Mumbai"]. - File-upload fixtures such as
lead["LeadWithFile"]._rec.
That keeps scenarios short and readable because large request bodies stay in testing.data instead of being repeated inline.
Using saveAs
When a step returns a value you want later, use saveAs.
The standard pattern is to save the full login response as loginSuccess:
{
"given": {
"op": "api.request",
"with": {
"method": "POST",
"url": "${env:TEST_API_BASE_URL}/api/iam/authenticate",
"json": {
"email": "libTestEditor@test.local",
"username": "",
"password": "Test@1234"
}
},
"saveAs": "loginSuccess"
}
}Later steps read the token via:
"Authorization": "Bearer ${res:loginSuccess.bodyJson.data.accessToken}"Saving the full response rather than only the token preserves the rest of the payload for later assertions or follow-up calls.
Scenario Chaining With util.require
A common SolidX pattern is:
- Create a reusable bootstrap scenario, usually authentication.
- Save its result with
saveAs. - Start later scenarios with
util.require. - Fail early with a helpful message if the prerequisite resource is missing.
Example:
{
"given": {
"op": "util.require",
"with": {
"resource": "loginSuccess",
"message": "Run scenario api-authenticate-success first to create loginSuccess."
}
}
}The venue module uses this pattern throughout its authenticated API scenarios.
Custom Specs
When built-in operations are not enough, use test.spec.
{
"when": {
"op": "test.spec",
"spec": "example.customHealth",
"with": {
"input": {
"url": "${env:API_BASE_URL}/health"
}
},
"saveAs": "custom.health"
}
}Custom specs are registered through testing.specs.
Real project pattern:
{
"specs": ["testing/register-test-specs.js"]
}That registrar then maps ids such as venue.customHealth to concrete implementations.
The venue example also shows a helpful convention where with.input includes both:
- Direct input values, such as a health URL.
- A resource path, such as
authResourcePath.
This lets a custom spec combine metadata input with previously saved runtime state.
Authoring Recommendations
Recommended practices:
- Keep scenario ids stable and descriptive.
- Use tags such as
smoke,regression, orauth. - Keep API and UI scenarios small and composable.
- Use
generate moduleorseedworkflows consistently before execution. - Prefer
saveAsplus interpolation over hard-coded chained values. - Reserve
test.specfor genuine escape-hatch cases. - Prefer reusable fixture libraries in
testing.dataover repeating large payloads inline. - Make scenario prerequisites explicit with
util.require. - Keep one small authentication bootstrap scenario per module when many scenarios need auth.
When To Use API vs UI vs Mixed
- Use
apiwhen you want fast, direct, backend-facing verification. - Use
uiwhen you want browser-level user-flow verification. - Use
mixedwhen your flow crosses both layers and it would be artificial to separate them.
Next
Continue with API Testing.

