dq-nbomber-cli
0.2.1
dotnet tool install --global dq-nbomber-cli --version 0.2.1
dotnet new tool-manifest
dotnet tool install --local dq-nbomber-cli --version 0.2.1
#tool dotnet:?package=dq-nbomber-cli&version=0.2.1
nuke :add-package dq-nbomber-cli --version 0.2.1
dq-nbomber-cli
All-in-one .NET global tool CLI for NBomber load testing.
Scaffold projects, generate scenarios from OpenAPI/GraphQL specs, run declarative YAML configs, validate, report, and enforce CI thresholds — with a single command.
Requirements
Install the tool:
dotnet tool install -g dq-nbomber-cli
Tip: During development you can skip the install and use
dotnet run --project src/DqNBomber.Cli --instead ofdq-nbomber.
Quick start — generate and run a load test from your OpenAPI spec
The example below targets a service running at http://localhost:3000 with its spec exposed at http://localhost:3000/api-docs.json.
Step 1 — Generate a load test config
dq-nbomber generate http://localhost:3000/api-docs.json \
--base-url http://localhost:3000 \
--output-dir ./my-loadtest \
--data-records 20
What this does:
- Fetches and parses your OpenAPI spec from the live URL
- Shows an interactive prompt — use Space to toggle endpoints, Enter to confirm
- Detects auth schemes and automatically inserts a login step that captures a JWT token
- Writes the following files to
./my-loadtest/:dq-nbomber.yaml— declarative load test configdata/users.csv— 20 rows of Bogus-generated fake users (email, password, firstName, lastName).env.example— environment variable stubs
To skip the interactive picker and include every endpoint:
dq-nbomber generate http://localhost:3000/api-docs.json \
--base-url http://localhost:3000 \
--output-dir ./my-loadtest \
--non-interactive
To include or exclude specific endpoints:
# Only POST and GET on /orders
dq-nbomber generate http://localhost:3000/api-docs.json \
--base-url http://localhost:3000 \
--output-dir ./my-loadtest \
--include "POST /orders,GET /orders/{id}"
# Exclude destructive endpoints
dq-nbomber generate http://localhost:3000/api-docs.json \
--base-url http://localhost:3000 \
--output-dir ./my-loadtest \
--exclude "DELETE *"
Step 2 — Review and edit the generated config
cat ./my-loadtest/dq-nbomber.yaml
A typical generated config looks like this:
scenarios:
- name: api_load_test
steps:
- name: login
http:
method: POST
url: ${BASE_URL}/auth/login
body:
json:
email: ${data.users.email}
password: ${data.users.password}
capture:
- jsonPath: "$.token"
as: authToken
- name: get_orders
http:
method: GET
url: ${BASE_URL}/orders
headers:
Authorization: Bearer ${capture.authToken}
loadSimulations:
- kind: inject
rate: 10
interval: "00:00:01"
during: "00:01:00"
thresholds:
- okRequest: "Percent > 95"
- okLatency: "P99 < 500"
report:
folder: reports
Set the BASE_URL env var (or copy .env.example to .env and edit it):
cd ./my-loadtest
cp .env.example .env
# edit .env: BASE_URL=http://localhost:3000
Step 3 — Validate the config
dq-nbomber validate ./my-loadtest/dq-nbomber.yaml
Expected output:
✓ Config is valid.
Step 4 — Run the load test
dq-nbomber run ./my-loadtest/dq-nbomber.yaml
Options:
| Flag | Default | Description |
|---|---|---|
--profile <name> |
— | Overlay config.<name>.yaml on top of the base config |
--target <name> |
— | Run only specific scenario(s) by name (repeat for multiple) |
--report-folder <path> |
reports/ |
Write reports to a custom root folder |
--display-console-metrics |
false |
Show real-time per-second metrics in the terminal |
--no-warmup |
false |
Skip the warm-up phase for all scenarios |
--log-level <level> |
normal |
NBomber log verbosity: quiet · normal · verbose |
Global --debug flag — place it before run:
# Trace every HTTP request and response while the test runs
dq-nbomber --debug run ./my-loadtest/dq-nbomber.yaml
For each step you will see:
[login] → POST https://api.example.com/auth
Content-Type: application/json
Body: {"username":"alice","password":"***"}
[login] ✓ ← 200 OK
content-type: application/json
Body: {"token":"eyJ..."}
[login] Captures:
✓ token = eyJ...
Sensitive headers (Authorization) and JSON fields (password, secret, token, apikey) are automatically redacted.
Suppress all NBomber output (CI mode):
dq-nbomber run ./my-loadtest/dq-nbomber.yaml --log-level quiet
Example with a profile:
dq-nbomber run ./my-loadtest/dq-nbomber.yaml \
--profile staging \
--display-console-metrics
This loads ./my-loadtest/config.staging.yaml and merges it on top of the base config.
Export to a runnable C# program
Once your dq-nbomber.yaml is validated, you can export it to a self-contained NBomber C# program:
dq-nbomber export ./my-loadtest/dq-nbomber.yaml
This writes two files into the same directory as the YAML:
| File | Description |
|---|---|
Program.cs |
Runnable NBomber 6.x C# program using NBomber.Data typed data feeds |
README.md |
Run instructions for the exported program |
The generated Program.cs:
- Uses
#:packagedirectives — no.csprojneeded with .NET 10+ - Loads data feeds with
Data.LoadCsv<T>()/Data.LoadJson<T[]>()andDataFeed.Circular() - Reads
BASE_URLand secrets from the.envfile at runtime - Requires
NBomber,NBomber.Http, andNBomber.Datapackages (auto-restored bydotnet run)
cd ./my-loadtest
# edit .env: set BASE_URL and any auth secrets
dotnet run Program.cs
Export options
| Option | Default | Description |
|---|---|---|
--target |
nbomber |
Export target. Currently: nbomber |
--format |
file |
file — single Program.cs with #:package (dotnet 10+); project — Program.cs + LoadTest.csproj (dotnet 8/9) |
--readme |
true |
Write a README.md alongside Program.cs |
Export to a project format (dotnet 8/9 compatible):
dq-nbomber export ./my-loadtest/dq-nbomber.yaml --format project
This creates LoadTest.csproj in addition to Program.cs so you can use dotnet run without .NET 10.
Trend reporting
Every dq-nbomber run writes its results into a timestamped subfolder (reports/{yyyy-MM-dd_HH-mm-ss}_{yaml}/) and saves a run-meta.json snapshot alongside the standard NBomber reports. The trend command reads all of those snapshots and produces:
- Console table — last N runs with per-step OK/Fail/RPS/P50/P95/P99, colour-coded latency, and ASCII sparklines.
- Interactive HTML dashboard — self-contained single-file report with Chart.js charts, opened in your browser.
# View last 20 runs for a specific config (console + generate HTML)
dq-nbomber trend --yaml dq-nbomber.yaml
# Open the HTML report immediately
dq-nbomber trend --yaml dq-nbomber.yaml --open
# Look at a different reports folder and show the last 50 runs
dq-nbomber trend --folder ./perf-reports --yaml dq-nbomber.yaml --last 50
# Write the HTML to a custom path (useful for CI artefacts)
dq-nbomber trend --yaml dq-nbomber.yaml --out ./ci-output/trend.html
Trend command options
| Option | Default | Description |
|---|---|---|
--folder |
reports |
Root folder containing timestamped run subfolders. |
--yaml |
— | Filter to runs generated from a specific YAML file (by base name). |
--last |
20 |
Maximum number of runs shown in the console table. |
--open |
false |
Open the generated HTML report in the default browser. |
--out |
reports/trend-{yaml}.html |
Custom output path for the HTML file. |
HTML dashboard features
The generated HTML file is fully self-contained — no server required, works offline.
| Feature | Description |
|---|---|
| KPI summary cards | Avg P95, Avg P50, Throughput, Error Rate, Total Requests — each with Δ% vs the previous run |
| Scenario tabs | One tab per scenario; each has its own charts and table |
| Chart.js line charts | P95, P50, P99, Mean latency · RPS · Failure Rate — one series per step |
| Inline sparklines | Per-row mini trend canvas highlighting where each run sits in the overall history |
| Δ P95% column | Red/green regression indicator showing percentage change vs the previous run |
| Run filter | Multi-select dropdown to show only selected run timestamps |
| Step filter | Multi-select dropdown to show only selected steps |
| Scenario filter | Multi-select dropdown above the tab bar to show/hide scenarios |
| Column sort | Click any column header to sort ascending/descending |
| Date range filter | 7 / 14 / 30 / 90 days or All time |
| Run comparison | Pick any two runs and diff all metrics side-by-side with red/green highlighting |
| CSV export | Download all filtered data as a .csv file |
| Run history table | Chronological list of all runs in the selected date range |
Reports folder structure
After running several tests the reports/ folder looks like:
reports/
2026-05-02_14-31-00_dq-nbomber/
dq-nbomber.csv ← NBomber per-step results
dq-nbomber.html ← NBomber single-run HTML
dq-nbomber.md
run-meta.json ← trend snapshot (auto-written by dq-nbomber run)
2026-05-02_14-44-00_dq-nbomber/
...
run-meta.json
trend-dq-nbomber_yaml.html ← generated by dq-nbomber trend
| Command | Description |
|---|---|
dq-nbomber init [path] |
Scaffold a new project with a starter dq-nbomber.yaml |
dq-nbomber generate <spec> |
Generate config from an OpenAPI or GraphQL spec |
dq-nbomber validate <config> |
Validate a config file |
dq-nbomber export <config> |
Export a validated YAML to a runnable NBomber C# program |
dq-nbomber run <config> |
Run load test scenarios |
dq-nbomber trend |
View interactive trend report across historical runs |
dq-nbomber report list |
List reports from past runs |
dq-nbomber report open |
Open the latest HTML report in a browser |
dq-nbomber thresholds check |
Run test and exit with code 2 on threshold violations |
dq-nbomber run <config> (with cluster: block) |
Run in distributed NBomber cluster mode (coordinator or agent) |
Run dq-nbomber <command> --help for full options on any command.
Config reference
Variable interpolation
Variables are resolved in this order (highest wins):
| Syntax | Resolved from |
|---|---|
${capture.varName} |
Value captured from a previous step's response |
${data.namespace.field} |
Current row from a data feed file |
${ENV_VAR} |
Process environment variable or .env file |
${ENV_VAR:-default} |
Env var with fallback default |
Load simulations
loadSimulations:
- kind: inject # inject N new vusers per interval
rate: 20
interval: "00:00:01"
during: "00:01:00"
- kind: keepconstant # maintain N concurrent vusers
copies: 50
during: "00:02:00"
- kind: rampinginject # ramp from 0 → rate over during
rate: 100
interval: "00:00:01"
during: "00:02:00"
- kind: rampingkeepconstant # ramp from 0 → copies over during
copies: 100
during: "00:02:00"
- kind: pause
during: "00:00:10"
Thresholds
thresholds:
- okRequest: "Percent > 95" # at least 95% of requests must succeed
- failRequest: "Percent < 1" # fewer than 1% failures
- okLatency: "P99 < 500" # 99th percentile under 500ms
- okLatency: "P95 < 200"
- okLatency: "RPS >= 30" # at least 30 requests per second
Data feeds
scenarios:
- name: my_scenario
dataFeeds:
- file: data/users.csv
namespace: users
strategy: circular # circular | random | unique
partition: false # true = slice rows by agent in cluster mode
steps:
- name: login
http:
method: POST
url: ${BASE_URL}/login
body:
json:
email: ${data.users.email}
password: ${data.users.password}
| Feed option | Default | Description |
|---|---|---|
file |
required | Path to .csv or .json file, relative to the YAML |
namespace |
data |
Variable prefix — ${namespace.column} |
strategy |
circular |
circular loops rows; random picks randomly; unique each row once |
partition |
false |
When true in cluster mode, each agent receives a distinct slice of the rows. Safe to leave true for single-node runs — no slicing applied |
Capture (cross-step variable extraction)
capture:
- jsonPath: "$.token" # simple dot path
as: authToken
- jsonPath: "$.data.id"
as: userId
- jsonPath: "$.results[0].sku" # array index
as: firstSku
- jsonPath: "$.items[?@.active==true && @.kind=='delivery-truck'].id" # RFC 9535 filter
as: itemId # use [?@.field==value] NOT [?(@.field==value)]
- header: "X-Request-Id" # extract from response header
as: requestId
- statusCode: true # capture HTTP status code
as: lastStatus
- regex: "id=([0-9]+)" # extract first regex group from body
as: resourceId
- cookie: "session" # extract Set-Cookie value
as: sessionCookie
JsonPath syntax note: The evaluator follows RFC 9535 (JsonPath.Net). Filter expressions must use
[?@.field==value]without outer parentheses. The=~regex operator is not supported — use theregex:extractor instead.
Distributed cluster mode (Kubernetes / multi-node)
Run load tests across multiple nodes using NBomber's built-in coordinator/agent model, backed by NATS as the message bus.
How it works
- Coordinator node orchestrates the run and collects results.
- Agent nodes execute the scenarios.
- All nodes use the same
dq-nbomber.yaml— only thecluster.nodeTypediffers. - Cluster settings are declared in a
cluster:block insidedq-nbomber.yamland are automatically forwarded to the NBomber runner. CLI flags always override YAML values.
YAML cluster: block
cluster:
nodeType: coordinator # coordinator | agent
natsUrl: nats://nats-service:4222
agentsCount: 3 # coordinator waits for this many agents
clusterId: my-run # shared ID — must match across all pods
localDev: false # true = single-machine local test
agentGroup: groupA # optional ManualCluster group name
coordinatorTarget: # scenarios assigned to coordinator
- api_load
agentTarget: # scenarios assigned to agents
- api_load
scenarios:
- name: api_load
steps: ...
cluster: values map 1-to-1 to NBomber's native CLI args. Any --cluster-* token passed after -- on the command line overrides the YAML value.
Run on Kubernetes (minikube or internal cluster)
1. Deploy NATS
kubectl run nats --image=nats:latest --port=4222
kubectl expose pod nats --port=4222
For internal clusters use a Deployment + Service and reference it as
nats://nats.<namespace>.svc.cluster.local:4222.
2. Build your image
The image must contain the .NET runtime, the dq-nbomber tool, and your YAML + data files.
FROM mcr.microsoft.com/dotnet/runtime:8.0
WORKDIR /app
COPY . .
RUN dotnet tool install -g dq-nbomber-cli --add-source /app/nupkg
ENV PATH="$PATH:/root/.dotnet/tools"
ENTRYPOINT ["dq-nbomber", "run", "dq-nbomber.yaml"]
3. Coordinator pod / job
# coordinator.yaml (Kubernetes Job)
apiVersion: batch/v1
kind: Job
metadata:
name: nbomber-coordinator
spec:
template:
spec:
containers:
- name: coordinator
image: your-test-image:latest
env:
- name: NBOMBER_LICENSE
valueFrom:
secretKeyRef:
name: nbomber-secret
key: license
restartPolicy: Never
The cluster: block in dq-nbomber.yaml already sets nodeType: coordinator.
Override at deploy time if needed:
kubectl run coordinator --image=your-test-image \
-- dq-nbomber run dq-nbomber.yaml -- --cluster-node-type=coordinator
4. Agent pods
# Create N agent pods (same image, same YAML, different nodeType)
for i in 1 2 3; do
kubectl run agent-$i --image=your-test-image \
-- dq-nbomber run dq-nbomber.yaml -- --cluster-node-type=agent
done
Or use a Deployment with replicas: 3.
5. Minikube quick test (single machine)
Set localDev: true in the YAML (skips NATS, runs coordinator + agents in-process):
cluster:
localDev: true
agentsCount: 2
Then run normally:
dq-nbomber run dq-nbomber.yaml
Coordinator console output
When a cluster: block is present, dq-nbomber run prints:
Cluster mode: node=coordinator nats=nats://nats-service:4222 agents=3
Precedence rules
| Source | Priority |
|---|---|
-- unparsed CLI tokens |
Highest (always override YAML) |
cluster: YAML block |
Applied if no matching CLI token |
| NBomber defaults | Lowest |
CI / CD integration
The run command exits with:
0— all scenarios passed thresholds1— config error or test error2— threshold violation
Example GitHub Actions step:
- name: Load test
run: |
dq-nbomber run dq-nbomber.yaml --profile ci
env:
BASE_URL: ${{ secrets.STAGING_URL }}
NBOMBER_LICENSE: ${{ secrets.NBOMBER_LICENSE }}
Development
# Build
dotnet build
# Run tests
dotnet test
# Run CLI without installing
dotnet run --project src/DqNBomber.Cli -- <command> [options]
License
This project is licensed under the GNU Affero General Public License v3.0.
You are free to use, modify, and distribute this software under the terms of the AGPL-3.0. If you run a modified version of this program as a network service, you must make the corresponding source code available to users of that service.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.