Writing tests

Test data

Values a scenario needs do not have to be typed into the feature file. Put them in JSON files under data/, and point at them from a step:

data/
  users.json
{
  "admin": { "email": "admin@test.com", "role": "Admin" },
  "roles": ["Admin", "Editor", "Viewer"]
}
When I sign in as {data:users.admin.email}
Then the profile should show the role {data:users.admin.role}
And the first role offered should be {data:users.roles.0}

A reference is the file name without .json, then the path to the value, separated by dots. A number picks an entry from a list.

You write It means
{data:users.admin.email} one value from data/users.json
every {data:users.roles} in a Then step check each value of a list
Examples: {data:roles} outline rows from data/roles.csv or a JSON list (.saffron only)
{unique:id} a value that is new on every run
--env staging lay data/*.staging.json over the base files

What does not belong in data/. Secrets. Data files are committed like the rest of your tests, so passwords and keys stay in {env:VAR}.

Where it works. Step text, data table cells and doc strings, in .saffron and .feature files alike, and inside step sets. The folder is data by default; "dataDir" in saffron.config.json moves it.

Single values

What happens to it. {data:...} is in the same family as {env:VAR} and {date+1}. While recording, the agent is given the real value; before the recording is saved, that value is turned back into the token. Every replay then reads the file again. So:

  • Change a value in the file, and the next replay uses it, at zero tokens. Nothing is recorded again.
  • If a recording still has the OLD value written out somewhere (the agent matched a sentence that contained it, say), changing the value makes that scenario stale and it records again, rather than quietly typing or checking yesterday's data. The same rule data tables follow.
  • A file or key that does not exist stops the run before any browser opens, naming the reference, what is missing and the scenario using it.
  • Where a step uses a value (typing it, matching it), the reference must point at ONE value; pointing at an object is an error that suggests the entry to use. A list is for the step below.

Checking a whole list: "every"

Point a Then step at a list and say that every value should be there:

Then the status filter should list every {data:enums.OrderStatus}
Then the roles table should show every {data:users.roles}

The recording holds ONE assertion that names the list and the place (dropdown, table, list, menu), not one check per value. Replay reads the list from the file each time, so adding "Refunded" to the JSON makes the next run check for it, at zero tokens, and a missing one fails by name: "Refunded" from {data:enums.OrderStatus} is not shown in the target (4 of 5 found). It checks that every listed value is present, matched as whole text ("Paid" is not satisfied by "Unpaid"); extra entries such as "All" or "Select…" are fine. It works on a closed dropdown too.

Examples from a file

In a .saffron file, a Scenario Outline can take its rows from the data folder instead of an inline table:

Scenario Outline: Role sees its menu
  Given I sign in as <role>
  Then I should see the <menu> menu

  Examples: {data:roles}

{data:roles} reads data/roles.csv, whose header row names the placeholders, or a JSON list of records ({data:users.accounts} works too). An outline records once and replays once per row, so adding a row to the file is one more zero-token replay, with nothing recorded. Rows are numbered "(example 1)", "(example 2)" like inline ones. A placeholder the file has no column for is an error naming both; a .feature file that tries this is told to rename itself to .saffron.

Values that must be new on every run: {unique:name}

A scenario that creates something (a user, an order, a project) collides with its own leftovers the second time it runs, unless what it types is new each time:

When I register as user-{unique:id}@test.com
Then the welcome banner should greet user-{unique:id}@test.com

{unique:id} is different on every run and the same everywhere inside one run, so a later step finds what an earlier step created. It is 10 lowercase letters and digits, starting with a letter; {unique:id:digits} is 9 digits for numeric fields. While recording, the agent types the generated value and Saffron turns it back into the token. When a run is not green, the report lists what each token stood for, so you can find the record it left behind.

Data per environment

--env staging (or SAFFRON_ENV=staging, or "env": "staging" in the config) lays data/users.staging.json over data/users.json: objects merge key by key, lists and plain values are replaced, and a file that has no staging version is used as it is. The step keeps saying {data:users.admin.email} on every target. For Examples, a roles.staging.csv replaces roles.csv whole. saffron status shows the selected environment and any reference that does not resolve in it.

In the editor

Both IDE integrations complete {data: (files first, then keys, showing each value), show the current value on hover, jump to the key in the data file, and underline a reference that does not resolve. {data:...} and {unique:...} are highlighted like {env:VAR}.