In most languages, testing is a second discipline: a framework to install,
a directory to arrange, fixtures to learn. In Crow, verification is part of
writing a function. A test block right under a fn states what that
function does — and what it can never do. Everything in it is ordinary
Crow you already know: comparisons, if, error values, maps, structs.
1 2 3 4 5 6 7 8 9 10 11 12
fn fee[notional]
if notional lt 0
return error['negative notional', 'validation']
return max[1.0, notional * 0.001]
test[n]
fee[50000.0] eq 50.0
fee[100.0] eq 1.0
fee[-5.0].kind eq 'validation'
if n gte 0.0
fee[n] gte 1.0
log['fee is defined']expected output — from the book, verified by crow test --book
fee is definedNotice what running that program printed: just the log line. Test blocks
never execute when a program runs — crow run skips them, crow build
strips them from the binary entirely. They run when you ask for
verification:
$ crow test pricing.co
✓ 1 block pass — 3 truths, 1 law, 5 inputs run
Each plain boolean line in a test block is a truth — something that must
be so. The first three lines above are examples: exact facts at inputs
you chose. fee[100.0] eq 1.0 records a decision (the fee floor is one
dollar), and the .kind line pins the error path — error values are
ordinary values (chapter 7), so verifying them needs nothing special.
A truth under a precondition (if s gt 0.0, and the comparison inside
it) is checked where the precondition holds. A block has to reach at
least one truth or assert, in its body or in a function it calls. One that reaches none fails with asserts nothing: a
test that cannot fail tests nothing, and a line that was meant as a truth
but is not one (a call whose value nobody compares) would pass forever.
When a truth fails, you see both sides:
⚠ 1 failing of 1 block
pricing.co:5
fee tests — line 6: assert failed: eq
left: 0.1
right: 1.0
Someone "simplified" fee and dropped the floor; the truth caught it with
the actual values, not just a red X.
The block's parameter — test[n] — is not an argument anyone passes. It
declares a generated input: the checker invents many values for n,
boundary cases first, and checks the lines that use it against every one.
fee[n] gte 1.0 is a law: true not at a point, but everywhere.
The if n gte 0.0 line is doing honest work. Without it, the checker
finds n = -1.0 — where fee returns an error, and errors have no truth
value, so the law fails loudly. That failure is correct: the law as first
written claimed too much. A precondition in a test block is just an if.
Examples pin decisions; laws pin relationships. Most functions only need examples. Write a law when your domain hands you one — "doubling the quantity doubles the price", "encoding then decoding returns the original."
A test with a name stands alone — it isn't about one function, it's
about functions working together. Setup, a sequence of calls, and
assert at each step:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
fn restock[stock, item, n]
if item in stock
return set[stock, item, stock[item] + n]
return set[stock, item, n]
fn take[stock, item, n]
if item not in stock
return error['no `item` in stock', 'stock']
if stock[item] lt n
return error['only `stock[item]` `item` left', 'stock']
return set[stock, item, stock[item] - n]
test restock_then_take[]
s = restock[map[], 'apples', 10]
after = take[s, 'apples', 4]
assert[after.apples eq 6]
short = take[after, 'apples', 100]
assert[not short.ok]
assert[short.kind eq 'stock']
log['module loaded']expected output — from the book, verified by crow test --book
module loadedInside scenarios you use assert for each step. assert is the one
verification word that also ships in production — a runtime guard —
which is exactly why the block keyword is test, not assert: one word
runs in your program, the other never does. A scenario can open a
database or a headless web session too; chapters 11 and 12 show both.
A named test with parameters is a stateful law — test webhook_twice[fill]
reads as "for any fill…" and the checker generates the fills.
Verification never creates methods. A test body calls your real
functions — that is what testing is — but you never define a function
for a test. Bindings, sequences, conditions: they all live inside the
block. If it's in the program's namespace, it's the program; if it's under
test, it's the rules. The boundary is the syntax, so nobody has to
police it.
crow test runs the blocks you wrote. crow check runs those plus
everything you didn't write: automatically generated edge cases for your
pure functions, determinism contracts, and — with a database url — the
concurrency hazard checks from the databases chapter. Tests are the part
of verification you author; check is all of it.
Next: put the last seven chapters to work on real data.