The Crow Programming Language

Testing

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 defined

Notice 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

Truths: bare booleans

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.

Laws: generated inputs

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."

Named tests: scenarios

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 loaded

Inside 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.

The one rule about where things live

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.

The verbs

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.