Skip to main content

Data-driven testing with CSV, TSV, and Excel

Write one workflow, keep the test data in a spreadsheet, and Paxter runs the workflow once per row. Each row becomes a case with its own report, and the suite report shows every case side by side. This is the fastest way to turn a manual test matrix into a regression suite.

A complete example​

cases.csv

CaseName,Email,Password,ExpectedMessage,ExpectedOutcome
Valid user,test.user@example.invalid,{{env:PAXTER_TEST_PASSWORD}},Welcome back,pass
Unknown user,nobody@example.invalid,anything,We could not find that account,pass
Locked account,locked@example.invalid,anything,Welcome back,fail
Not ready yet,draft@example.invalid,anything,,pass

sign-in.yaml

Name: Sign-in regression
Headless: true
NeedVideo: false
DataSource:
Path: cases.csv
Actions:
- Do: Page_GotoAsync
Data1: https://your-approved-test-environment.example/sign-in
- Do: Fill
Target: label=Email address
Value: "{{Email}}"
- Do: Fill
Target: label=Password
Value: "{{Password}}"
Secret: true
- Do: Click
Target: button=Sign in
- Do: Expect_Text
Target: role=status
Value: "{{ExpectedMessage}}"
paxter validate sign-in.yaml
paxter run sign-in.yaml

Paxter validates the workflow against the data first (unknown columns, unquoted placeholders, missing files), then runs the rows in order and prints one line per case:

Case 1/4 "Valid user": SUCCESS - 5/5 steps (3.1s)
Case 2/4 "Unknown user": SUCCESS - 5/5 steps (2.8s)
Case 3/4 "Locked account": SUCCESS - failed as expected (7.9s)
Case 4/4 "Not ready yet": SKIPPED - marked Skip in the data source

Placeholders​

Use {{ColumnName}} anywhere in Target, Value, Key, Mode, Data1, Data2, Data3, or a step Name. Column names are matched case-insensitively.

  • Quote values that start with a placeholder. YAML reads an unquoted {{ as a mapping. Write Value: "{{Email}}". The alternative spelling ${Email} needs no quotes.
  • Placeholders are substituted when the cases are expanded, so the report for each case shows the real values that were used. Columns whose names contain password, passcode, secret, token, apikey, api_key, or credential are masked in reports, output.json, and the suite results.
  • A missing column is a validation error with a suggestion ({{Emial}} becomes Did you mean {{Email}}?). The browser is never launched for an invalid suite.

Other placeholder sources work alongside data columns:

PlaceholderResolves to
{{env:NAME}}The environment variable NAME at run time. Never written to reports, which show the placeholder text. Use it for passwords and API keys.
{{__name}}A runtime variable stored by DefineVariable, ReadInnerText, or ReadValue in an earlier step.
{{row.index}}The 1-based data row number.
{{row.name}}The case name.
{{today}}, {{now}}, {{timestamp}}yyyy-MM-dd, HH:mm:ss, yyyyMMddHHmmss.
{{guid}}, {{random.name}}, {{random.number}}Unique or random values for creating records.

Values in the data file can themselves contain {{env:...}} and {{__variable}} placeholders, as the password column above shows.

Reserved columns​

ColumnEffect
CaseNameNames the case in the console, the suite report, and the case folder. Defaults to Row N.
Skiptrue, yes, y, or 1 skips the row. It appears in the suite report as skipped.
ExpectedOutcomepass (default) or fail. With fail, the case passes only when the workflow fails; use it for negative tests such as invalid input being rejected. Any other value is a validation error.

Reserved columns remain available as placeholders.

Data source settings​

DataSource:
Path: data/cases.xlsx # relative to the workflow file, or absolute
Sheet: Regression # Excel only: sheet name or 1-based index; default first sheet
Delimiter: auto # auto, comma, tab, semicolon, pipe, or one character
HasHeader: true # false names columns Column1, Column2, ...
Rows: 1-10,15 # optional 1-based selection; default all rows
MaxRows: 500 # safety limit per run (hard maximum 5,000)
FormatNotes
.csvComma-separated, quoted fields, doubled quotes, embedded line breaks, UTF-8 with or without BOM. Lines starting with # are comments.
.tsv, .tab, .txtTab-separated by default.
.xlsx, .xlsmExcel workbooks. Text, numbers, booleans, and formula results are read. Dates arrive as Excel serial numbers, so format date columns as text in the sheet or keep them as ISO strings. .xls is not supported; save as .xlsx or .csv.

Keep the header in the first row, avoid merged cells, and keep one case per row.

Command-line overrides​

paxter run sign-in.yaml --data smoke-cases.csv
paxter run sign-in.yaml --rows 3
paxter validate sign-in.yaml --data cases.xlsx --rows 1-5

--data replaces DataSource.Path, and --rows replaces DataSource.Rows. A workflow without a DataSource block becomes data-driven when --data is supplied.

Sequential and parallel execution​

paxter run executes the rows one after another, each in a fresh browser session. paxter runparallel (subscription) runs rows concurrently up to MaxParallelTests, and TestIterations greater than 1 repeats every row that many times. Only run parallel suites against an environment that can safely take the load.

Output​

<OutputPath>/suite_2026-09-26_14-05-12/
suite-report.html one row per case with status, data, expected and actual outcome
suite-results.json the same data for CI dashboards (sensitive columns masked)
case_001_valid-user/
index.html the full case report with a Case data panel
output.json
resources/screenshot-1.png ...
case_002_unknown-user/
case_003_locked-account/

Screenshot and video paths inside a case folder are relative, so the whole suite folder can be zipped and opened elsewhere.

The process exit code is 0 when every executed case met its expected outcome and 1 otherwise, which is what a CI job needs.

Checking every row against its own expected result​

Assertions accept placeholders, so each row can carry its own expected values. Common patterns:

# Text on the page after the action
- Do: Expect_Text
Target: css=.total
Value: "{{ExpectedTotal}}"
Mode: exact

# A value read from the page compared with the row
- Do: ReadInnerText
Target: css=.reference-number
Data2: __reference
- Do: Expect_Equals
Data1: "{{__reference}}"
Data2: "{{ExpectedReference}}"

# The URL after a redirect
- Do: Expect_Url
Data1: "{{ExpectedPath}}"

# A negative case: put fail in the ExpectedOutcome column and assert the happy path

See Workflow actions for every assertion and Targets for the target syntax.