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.
What is an oath
Section titled “What is an oath”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.
What is an example
Section titled “What is an example”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.
Where an example begins and ends
Section titled “Where an example begins and ends”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.
Tables and doc strings
Section titled “Tables and doc strings”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:
```textHello, 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.
Header-bound tables
Section titled “Header-bound tables”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
Section titled “Headings”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.
Naming
Section titled “Naming”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.
Sentences, emphasis, and formatting
Section titled “Sentences, emphasis, and formatting”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.
Expected-to-fail examples
Section titled “Expected-to-fail examples”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.
```errorinsufficient 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.
Drift detection
Section titled “Drift detection”“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.
The baseline file
Section titled “The baseline file”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.
Accepting drift
Section titled “Accepting drift”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.
Why it’s important
Section titled “Why it’s important”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.