Names a single test case. The runner prints this name in pass and fail output. Keep it short and human.
test: "Login returns a valid session"
One file. Five runtimes. Same result. This spec defines the grammar, the type rules and the runner contract for TestML. Implementations that match it can claim conformance. The text below is normative.
TestML files use indented blocks. Two spaces per level. No tabs. Comments start with a hash. Strings use double quotes. The rest is data. This block is the canonical reference grammar.
# TestML v0.4 — abstract syntax (EBNF subset) file ::= block { block } ; block ::= key ":" ( value | indent_block ) ; key ::= /[a-z][a-z0-9_]*/ ; value ::= string | number | bool | null | regex ; string ::= '"' { utf8_char } '"' ; regex ::= "/" { regex_char } "/" [ flags ] ; indent_block ::= NEWLINE INDENT block { block } DEDENT ; # Whitespace and comment rules comment ::= "#" { any_char } NEWLINE ; indent ::= two spaces (tabs forbidden) ;
Every test file is built from these six top-level keys. A runner that reads them all and emits the right output is already conformant. Each card lists the shape and a short sample.
Names a single test case. The runner prints this name in pass and fail output. Keep it short and human.
test: "Login returns a valid session"
Describes the action under test. A method, a path, and an optional body. No setup hidden in code.
when: POST: "/api/login"
Lists every assertion the runner must check. Status codes, JSON shapes, regex matches and ranges.
then:
status: 200
json.session: /sess_[a-z0-9]{16}/Pairs inputs with outputs. The runner expands each row into its own test case. One file, many runs.
expects:
- { in: 2, out: 4 }
- { in: 3, out: 9 }Names reusable data blocks. Any test in the file can pull a fixture in by name. No copy and paste.
fixtures:
admin:
role: "admin"Links to other TestML files. The runner resolves the path against the suite root. No language hooks.
import: - "./shared/auth.tml"
TestML keeps the value set small on purpose. Scalars match JSON. Patterns add regex and ranges. Structures stay flat. Any runner that handles these eight types covers the full surface.
The runtime contract is the part that keeps results portable. A run in Python and a run in Java must produce the same pass and fail lines. The five steps below define that path.
Read the .tml file as UTF-8. Build an AST. Reject unknown top-level keys with a clear line number.
Walk every expects: block. Stamp out one test case per row. Inherit fixtures from the parent file.
Merge named fixtures into each test scope. Later keys win. Cycles raise a clean error, not a stack trace.
Execute every test in source order by default. Parallel mode is allowed but must keep the same report format.
Print one line per assertion. Pass lines start with a dot. Fail lines show the expected and the actual side by side.
We grade runners against the public conformance suite. Bronze is the minimum bar. Silver covers fixtures and matrix expansion. Gold runners also support parallel runs and JUnit output.
Parse the grammar and run the core constructs. Pass the public conformance suite at the base tier.
All Bronze rules plus fixtures, imports and matrix expansion. Report timings per test in milliseconds.
All Silver rules plus parallel runs, partial reruns and JUnit-compatible output. The badge ships with the runner.
Each row lists the runner, the install command and the tier it last passed. We re-run the suite on every spec release.
The spec lives in the open. Send a pull request when you spot a gap. Open an issue when wording is unclear. New language runners are welcome at any tier.