Skip to content

Examples

In Varar the Markdown is the test. An oath file is ordinary Markdown — prose, headings, lists, tables, code blocks — and Varar runs the parts of it that match your step definitions. This page is the reference for the unit it runs: the example. It covers exactly what becomes an example, where one begins and ends, how tables and doc strings attach, what role headings play, and how Varar notices when something that used to be an example no longer is.

A file is an oath if — and only if — its path matches the docs globs in varar.config.json. That config is the single source of truth, consulted by the runner, the LSP, and the vitest plugin alike:

{
"docs": {
"include": ["**/*.md"],
"exclude": ["README.md", "not-implemented/**"]
}
}

Both include and exclude are plain globs (no ! prefix). include has no default — an empty list discovers nothing; exclude removes matches from it. There is no special file extension: a plain .md file is an oath purely because the globs select it. Any Markdown file the globs don’t select is invisible to Varar.

Within an oath, the candidate unit is a paragraph — or a list item, or a blockquote. Varar splits each candidate into sentences and matches those sentences against your registered stimuli and sensors.

  • At least one sentence matches a step → the paragraph is an example. Its matching sentences run as steps, in order.
  • No sentence matches → the paragraph is prose. Varar reads past it: no test, no failure.

This is what lets a single file interleave narrative explanation with executable examples. A heading, an introductory paragraph, an aside — all sit happily beside the paragraphs that actually drive the system. Only the matching ones run.

A paragraph that matches at least one step but not every sentence is still one example. The unmatched sentences are narration that travels with the test — they even become part of its name (see Naming). Varar never guesses that an unmatched sentence “should have” been a step; there is no keyword sniffing, no “missing step” inference from sentence shape.

An example is everything between two delimiters. There are exactly three:

  • a heading (#, ##, …),
  • a thematic break (---, a horizontal rule), and
  • a paragraph that matches no step (prose).

Everything between two delimiters that has at least one matching paragraph is a single example. Consecutive matching paragraphs — with the tables and doc strings attached to them — merge into that one example, sharing state across every step:

I deposit 100.
I withdraw 30.
The balance is 70.

Three paragraphs, no delimiter between them → one example, three steps, one accumulating state. Keeping the sentences in a single paragraph does the same thing; a blank line between two matching paragraphs no longer starts a new example.

Because prose is a delimiter, narration separates examples with no special syntax — an introductory paragraph before an example, or an aside between two of them, ends whatever precedes it just by not matching a step.

To keep two step-only examples apart — matching paragraphs with nothing but blank lines between them — put a delimiter between them, or they run as one shared-state example:

I deposit 100. The balance is 100.
---
I deposit 5. The balance is 5.

Two examples, each starting from fresh state.

One edge to know: a prose paragraph placed between the steps of one example ends it — the steps after the prose start a fresh example and do not see the earlier state. Keep narration before or after an example, not in the middle of it.

A table or a fenced code block (a doc string) is not an example on its own. It attaches to the example immediately above it and is handed to that example’s last matched step:

Greet Bob:
```text
Hello, Bob!
```

The paragraph Greet Bob: is the example; the fenced block is the doc string its sensor compares against. A table attaches the same way:

These are the accounts:
| owner | balance |
| ----- | ------: |
| Alice | 100 |
| Bob | 50 |

Attachment is positional and adjacency-based:

  • The table/fence must directly follow the step-bearing block — nothing (no heading, thematic break, or other paragraph) between them.
  • It attaches to the last matched step of the block above it (so a paragraph with several steps hands the table to the final one).
  • A table or fence that has no step-bearing block above it is just Markdown content — not an error, not a test.

A single example can carry a whole Given → table → When → doc string flow. Consecutive matching paragraphs merge into one example (see Where an example begins and ends), and each paragraph’s table or doc string attaches to it — so an arrange-with-a-table, act-with-a-doc-string sequence stays one test. Separate the paragraphs with blank lines, exactly as ordinary Markdown (and every formatter) expects:

Given the following users have been imported:
| email | name |
| ----- | ---- |
| a@b.c | Ada |
And the following assets have been imported:
| name |
| ----- |
| Moose |

Both tables belong to the one example, and the file renders correctly on GitHub and survives prettier.

For the return-value rules — how a step reproduces a table cell-by-cell, or a doc string including its trailing newline — see Check tables and doc strings and the sensors reference.

There is one case where a table produces examples by itself: when the paragraph above it names every column of the table (each header cell appears as a whole word in the sentence). Then the table is header-bound — it iterates row by row, and each data row becomes its own example:

Each row gives a decimal and a roman number:
| decimal | roman |
| ------: | :---- |
| 3 | III |
| 9 | IX |
| 40 | XL |

The bound step runs once per data row, receiving the row as an object keyed by the header, and each row is named after its cells (3 / III, 9 / IX, …). This is the data-driven shape covered in Check tables and doc strings.

Headings never delimit an example and never name one. They are scope markers. The chain of headings above an example — outer to inner — becomes a stack of describe(...) groups around the test at runtime:

# Account
## Withdrawals
I deposit 100. I withdraw 30. The balance is 70.

runs the example nested under Account → Withdrawals. Deeper or equal-level headings pop the stack the way document outlines do. A heading also closes any open example, so an example never spans a heading.

An example’s name is its first matching paragraph in full, not a heading and not just the matched sentence:

I deposit 100. I withdraw 30. The balance is 70.

is named I deposit 100. I withdraw 30. The balance is 70 — the whole paragraph, because the narration around the matched sentences is part of what the example is about. When an example spans several merged paragraphs, the first matching one names it. Hard line breaks inside the paragraph collapse to single spaces (a name is one line), and a single trailing ., !, or ? is stripped; terminators inside the sentence (a quoted string, i.e.) are left alone.

Varar splits a paragraph into sentences on ., !, ?, and hard line breaks, with a few guards so real prose doesn’t fragment:

  • Terminators inside backtick code spans or double-quoted strings don’t split — The message is "Hello, world!" stays one sentence, so a {string} parameter captures it whole.
  • A . inside a number (3.14), a known abbreviation (e.g., i.e., etc., cf., vs.), or before a lowercase letter is not a boundary.

Matching runs against the raw text of the sentence — inline markup and all. I withdraw *30* is four characters longer than I withdraw 30 and matches only an expression that says so. Block markers are different: they are structure, not text, so a list item’s - bullet and a blockquote’s > prefix never reach the matcher. If a marked-up run is data, make it a custom parameter whose regexp includes the markers and whose parse strips them — markup is notation, and Varar never edits your prose behind your back.

A fenced block tagged error marks its example as expected to fail — the runner inverts the outcome, so the example passes only if a step actually fails. The block’s text, if any, is a substring the failure message must contain:

I withdraw 200 from my empty account.
```error
insufficient funds
```

An error fence is consumed by this rule — it is never treated as a doc string. If an example has an error fence but no runnable step to produce the failure, that is flagged as an authoring mistake.

“No match means prose” has a dangerous edge. A paragraph that was an example can stop matching — you rename a step, delete a step definition, or a typo creeps into the Markdown — and it silently reverts to prose. The suite stays green while testing less than it did: coverage decaying without a single failing test.

Varar treats this transition — was an example, now matches nothing — as drift, and refuses to let it pass unnoticed.

To recognise it, Varar records a fingerprint of each oath’s source in a baseline file, varar.lock.json, at the root of your project: an FNV-1a hash written as fnv1a:…. It is a tiny, dependency-free change-detector, computed identically in every Varar runtime, so drift is recognised the same way whatever language you run in. When an oath changes such that a previously matching paragraph no longer matches any step, Varar compares the new source against the recorded baseline and reports the drifted example by name and line.

Drift is never resolved silently. Varar will not drop the example for you (losing coverage), and it will not let the run pass as if nothing happened. The run fails until you explicitly acknowledge the drift — confirming that yes, this paragraph is intentionally no longer an example — much as you accept an updated snapshot. Acknowledging records the new baseline and the run goes green.

The distinction is deliberate:

  • A paragraph that never matched is prose — ignored, no ceremony.
  • A paragraph that stops matching is drift — surfaced, and held until you explicitly accept it.

varar.lock.json is written and updated by your test runner, in the directory it runs in. Commit it. Drift detection needs history to tell intentional prose from a regression, and having the baseline in version control is what makes an acknowledgment visible in review instead of swallowed.

Acknowledging drift re-records the baseline, much as you accept an updated snapshot:

Runner Accept drift with
vitest VARAR_UPDATE=1
pytest --varar-update, or VARAR_UPDATE=1
unittest, minitest, RSpec, go test, cargo test VARAR_UPDATE=1
JUnit, Kotest -Dvarar.update=true, or VARAR_UPDATE=1

VARAR_UPDATE accepts 1 or true.

Every runner records the baseline on a clean run, and VARAR_UPDATE=1 both lets a drifted run go green and re-records it. Commit the result. Editors can also accept a single drifted example — see Accept as prose.

An unacknowledged drift never rewrites the baseline: the old entry stays, so the run keeps failing until you accept it or restore the step.

The whole promise of Varar is that the Markdown is the test. If an example could quietly stop being a test, that promise leaks: a document that reads like a passing specification would actually be verifying nothing, and no one would know. Silent coverage loss is the worst failure mode a test system has, because it looks exactly like success — a green suite testing less than it claims.

A plain “missing step” warning in an editor doesn’t close the gap: a developer who never opens the editor never sees it, and CI never gates on it. Drift detection lives in the runner, so the same vitest / pytest / go test that proves your examples pass also proves that your examples are still examples. Distinguishing intentional prose (never matched) from a regression (stopped matching) needs history, which is exactly what the fingerprinted baseline provides — and requiring an explicit acknowledgment makes the change visible in review rather than swallowed.