Core Concepts
This page explains the foundational concepts behind BitDiver: how its modules fit together, how steps execute, and how data flows through a test run.
Architecture Overview
BitDiver is a single TypeScript package (@xhubio/bitdiver-runner) composed of 7 modules that form a pipeline:
- Config — Loads and validates configuration using Zod schemas. Supports environment variable overrides, inline values, file-based defaults, and automatic secret masking.
- Suite Builder — Reads YAML or JSON configuration files and constructs a suite definition. Handles timed file scanning and step generation from filename prefixes.
- Definition — TypeScript interfaces that describe the serialized structure of suites, test cases, and steps. Provides JSON schema validation for suite definitions.
- Model — Core execution model containing step classes (Normal, Single), built-in timing steps (Wait, DetermineStartTime, CheckStartTime), environments (Run and Testcase), and the StepRegistry.
- Runner Server — The execution engine. Sorts steps by dependency, manages parallel execution, reports progress, and orchestrates the full lifecycle.
- Check — Automatic result comparison using deep object diffing. Supports directives for Time, Number, Regex, and Contains matching. Generates summary and detail reports.
- Log Adapter — Pluggable logging with Console, ConsoleJson, File, and Memory adapters. Provides structured log messages with metadata and source tracking.
These modules work together as a pipeline: Config feeds validated settings into the Suite Builder, which produces a Suite Definition. The Runner Server takes that definition and executes it using the Model classes. During execution, the Check module compares actual results against expected values, and the Log Adapter captures everything that happens along the way.
Steps
Steps are the basic execution units in BitDiver. Every piece of test logic lives inside a step. Steps follow a defined lifecycle:
start() → beforeRun() → run() → afterRun() → end()
The start() and end() methods run once per step instance — they are called at the beginning and end of the step's execution regardless of how many test cases exist. The beforeRun(), run(), and afterRun() methods run once per instance: for a StepNormal, that means once per test case; for a StepSingle, once for the entire suite.
StepNormal
Runs once per test case in the suite. Access the current test case context via the typed this.tc getter. Use this for data-driven operations like sending HTTP requests, interacting with UIs, or validating individual responses.
StepSingle
Runs once for the entire suite, regardless of how many test cases exist. Access all test cases via the typed this.testcases getter. Use this for shared setup and teardown operations like initializing a database, starting a server, or cleaning up after all tests complete.
Runner-Managed Timing
Timing is managed by the Runner itself using three built-in steps that are pre-registered in the StepRegistry:
StepDetermineStartTime— Calculates a reference time by rounding up to the next full minute, accounting for an optional offset and per-testcase delay. Stores the result inEnvironmentRun.StepCheckStartTime— Verifies that the timing budget has not been exceeded. If the reference time has already passed, the step logs a fatal error and aborts the run. Otherwise it waits until the reference time is reached.StepWait— A configurable wait step. Accepts{ seconds: number }as data. Delays are skipped intestMode.
Timed steps in the suite definition 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.
Environments
BitDiver provides two environment types that act as data stores during a test run:
EnvironmentRun
Shared across the entire run. Every step in every test case has access to the same EnvironmentRun instance. It stores global configuration and shared state via its map property (a Map). Use it to share data between steps that need to coordinate — for example, storing an API token obtained during login so that subsequent steps can use it.
EnvironmentTestcase
Scoped to a single test case. Each test case gets its own EnvironmentTestcase instance with a name, a status, and a map for storing data. Steps running within the same test case share the same EnvironmentTestcase, allowing them to pass data forward (e.g., a request step stores the response, and a validation step reads it).
Status tracking follows a one-way escalation model — the status can only go up, never down:
OK → WARNING → ERROR → FATAL
If a step sets the status to ERROR, a later step cannot reset it to OK. This ensures that the final status always reflects the worst outcome encountered during the run.
Suite Builder
The Suite Builder creates suite definitions from configuration using a three-phase model:
- Setup — Sequential steps for preparation (database clearing, config loading, etc.)
- Timed — Auto-generated from test data files scanned in testcase directories, or defined explicitly. Files matching
<TIME>_<TYPE>_*.jsonare parsed and mapped to step classes viatimedStepMapping. Custom regex patterns with a<TIME>placeholder are also supported. - Teardown — Sequential steps for cleanup and reporting.
Step Entries with Parameters
Setup and teardown steps can be specified as simple strings or as objects with inline parameters. Parameters become step data for all testcases:
{
"setup": [
"SetupEnvironmentRun",
{ "step": "Wait", "seconds": 30 },
"ClearDatabase"
],
"teardown": [
"GenerateReport"
]
}
Suite Definition
A suite definition is a data structure (produced by the Suite Builder or created manually) that tells the runner which steps to execute and with which data. It has three key parts:
steps
An ordered list of step names. This determines the execution sequence — the runner processes steps in the order they appear in this array.
stepDefinitions
A map from step names to their configuration. Each entry specifies the step class (as registered in the StepRegistry) and the step type (normal, single, or timed).
testcases
An array of test case objects, each with a name and a sparse data map. The data map uses sparse storage: instead of including null entries for steps that have no data, only steps with actual data are present in the map. This keeps suite definitions compact, especially when many steps do not require per-testcase input.
For example, if a suite has 10 steps but a particular test case only provides data for steps 2 and 7, the data object will only contain keys for those two steps. The runner handles the absence of data gracefully — steps without data simply receive no input.
StepRegistry
The StepRegistry maps step names to their class implementations. Before the runner can execute a suite, every step name referenced in the suite definition must have a corresponding class registered in the StepRegistry.
import { StepRegistry } from '@xhubio/bitdiver-runner'
const registry = new StepRegistry()
registry.registerStep({ stepName: 'login', step: LoginStep })
registry.registerStep({ stepName: 'fetchData', step: FetchDataStep })
registry.registerStep({ stepName: 'validate', step: ValidateStep })
The name used during registration must exactly match the name used in the suite definition's stepDefinitions and steps arrays. If the runner encounters a step name that is not in the registry, it will throw an error before execution begins.
Persistence
Steps can persist data to and from disk, which is useful for sharing state between separate test runs or for debugging. BitDiver provides four persistence methods:
writeVars()
Saves variables from the step's context to JSON files on disk. The data remains in memory after writing, so the step can continue to use it.
loadVars()
Loads previously saved variables from JSON files on disk back into the step's context. Use this to restore state from a prior run or from a setup phase.
exportVars()
Saves variables to disk and then removes them from memory. This is useful when dealing with large data sets that you need to persist but do not want to keep in memory for the remainder of the run.
loadTempVars()
Loads variables from disk into the step's context, and the loaded data is automatically cleaned up after the step finishes running. This is useful for one-time reads where you need the data only during a single step's execution.