Step Types in Detail
BitDiver provides two primary step types: StepNormal for per-testcase work and StepSingle for suite-wide operations. Timing is handled by the Runner using built-in steps. All step types share the same lifecycle and configuration options.
Step Lifecycle
All steps follow the same lifecycle:
start() → beforeRun() → run() → afterRun() → end()
start()andend()— Called once per step (not per instance). Use these for one-time setup and teardown.beforeRun(),run(),afterRun()— Called per instance. For aStepNormal, this means once per test case. For aStepSingle, once per suite.
StepNormal
The most common step type. A StepNormal runs once per test case in the suite. Each invocation receives its own test case context.
import { StepNormal } from '@xhubio/bitdiver-runner'
class SendRequest extends StepNormal {
async run() {
const url = this.tc.map.get('url')
const response = await fetch(url)
this.tc.map.set('response', response)
await this.logInfo({ message: 'Request sent', url })
}
}
Key Properties
this.tc—EnvironmentTestcase(typed getter). The current test case context with its own map for storing data.this.data— Step data from the suite definition. Whatever was provided in thetestcases[].datafor this step.this.environmentRun— Run-wide state shared across all steps and test cases.this.logAdapter— The logging adapter for structured output.
StepSingle
Runs once for the entire suite rather than per test case. Use this for shared setup/teardown operations like database seeding, environment provisioning, or aggregate reporting. A StepSingle has access to all test case environments at once.
import { StepSingle } from '@xhubio/bitdiver-runner'
class ClearDatabase extends StepSingle {
async run() {
await db.truncate('results')
for (const { environment, data } of this.testcases) {
environment.map.set('dbCleared', true)
}
await this.logInfo('Database cleared')
}
}
Key Properties
this.testcases— Array of{ environment: EnvironmentTestcase, data: any }. Gives you access to every test case environment and its associated data.- Failure in a single step fails the entire run. Use single steps only for operations that are truly global.
Built-in Timing Steps
Timing is managed by the Runner using three built-in steps that are pre-registered in the StepRegistry. You do not need to register them manually — they are available by default under the names Wait, DetermineStartTime, and CheckStartTime.
StepDetermineStartTime
Extends StepSingle. Calculates a reference time for subsequent timed steps. The reference time is rounded up to the next full minute, accounting for an optional offset and per-testcase delay. The result is stored in EnvironmentRun under the REFERENCE_TIME_KEY constant.
// In your suite definition, use with inline parameters:
{ "step": "DetermineStartTime", "offsetSeconds": 60, "delaySeconds": 5 }
StepCheckStartTime
Extends StepSingle. Verifies that the timing budget has not been exceeded. If the reference time has already passed (the setup phase took too long), the step logs a fatal error and aborts the run. Otherwise it waits until the reference time is reached. In testMode, waiting is skipped but overrun detection still applies.
StepWait
Extends StepSingle. A simple configurable wait step. Accepts seconds as data. In testMode, the delay is skipped entirely.
// In your suite definition:
{ "step": "Wait", "seconds": 30 }
Runner Timing for Steps
Steps in the suite definition can carry a timing property with an offsetSeconds value. Before executing each timed step, the Runner waits until referenceTime + offsetSeconds is reached. All delays are bypassed in testMode.
StepTimed (Deprecated)
The legacy StepTimed class still exists for backwards compatibility. It extends StepNormal and requires implementing getReferenceTime(), getOffsetSeconds(), and doRun(). New suites should use Runner-managed timing instead.
import { StepTimed } from '@xhubio/bitdiver-runner'
// DEPRECATED — use Runner-managed timing instead
class SendAtTime extends StepTimed {
getReferenceTime(): string {
return this.tc.map.get('START_TIME')
}
getOffsetSeconds(): number {
return this.data.offsetTime
}
async doRun(): Promise<void> {
await sendMessage(this.data.payload)
await this.logInfo(`Sent at offset ${this.getOffsetSeconds()}s`)
}
}
Persistence in Steps
Steps can persist and load variables across executions using built-in persistence methods. These are available on both StepNormal and StepSingle:
writeVars(vars)— Persists variables to disk so they survive across runs. Use this to store credentials, session tokens, or any state that should be available in later suites.loadVars()— Loads previously persisted variables back into the step. Returns the stored object.exportVars(vars)— Exports variables to the run environment so other steps in the same run can access them.loadTempVars()— Loads temporary variables that were exported by a previous step in the same run. UnlikeloadVars(), these do not persist to disk.
// In a StepNormal: persist an auth token
async run() {
const token = await authenticate(this.data.credentials)
await this.writeVars({ authToken: token })
}
// In a later step: load the persisted token
async run() {
const vars = await this.loadVars()
const token = vars.authToken
// use token...
}
// Export vars for other steps in the same run
async run() {
await this.exportVars({ sessionId: '12345' })
}
// Load exported vars from a previous step
async run() {
const temp = await this.loadTempVars()
const sessionId = temp.sessionId
}
Step Options
All step types accept constructor options that control their behaviour within the runner:
name— A human-readable name for the step, used in logs and reporting.logAdapter— Override the default log adapter for this specific step.needData: false— Step runs even without data in the suite definition. By default, steps are skipped for test cases that provide no data for them.runOnError: true— Step runs even if the test case has already failed in a previous step. Useful for cleanup or reporting steps that must always execute.maxParallelSteps— Override the global parallelism limit for this step. Set to1to force serial execution, or increase it for I/O-heavy steps.
class CleanupStep extends StepNormal {
constructor(opts) {
super({
...opts,
name: 'cleanup',
needData: false,
runOnError: true,
maxParallelSteps: 1
})
}
async run() {
await cleanupResources()
}
}