SCUA has a built-in test runner. You write test functions in
*_test.scua files, assert what you expect, and run
scua test. It reports pass/fail and exits non-zero on
failure, so it fits straight into CI.
#A first test
Put your tests in a file whose name ends in _test.scua,
and write each test as a function named test_*:
-- inventory_test.scua
fn add_item(bag, item)
bag.push(item)
return bag
end
fn test_add_one()
let bag = add_item([], "sword")
assert_eq(len(bag), 1)
end
fn test_add_two()
let bag = add_item(add_item([], "sword"), "shield")
assert_eq(bag[1], "shield")
end
Run the tests from the directory that holds them:
$ scua test
PASS ./inventory_test.scua test_add_one
PASS ./inventory_test.scua test_add_two
2 passed, 0 failed (1 file)
scua test finds every *_test.scua file
under the current directory (or a directory you name:
scua test tests/, or a single file:
scua test one_test.scua). You can name several — files,
directories, or a mix — and all of them run:
scua test tests/ extra_test.scua. Each test_*
function runs on its own, so one failing test doesn't stop the
others.
If your project has dependencies — a scua.toml and a
vendored deps/ — your tests resolve them exactly as running
the program does; there is nothing to configure. For a tree with no
lock, --mod-path DIR adds a module search directory. Those
are the only options scua test takes, and it will tell you
so rather than ignore one it does not understand.
#Assertions
The runner doesn't need a special assertion library; the
assert family is built in.
assert(cond, msg?)faults withmsgunlesscondis truthy.assert_eq(got, want)faults unless the two values are equal, and the message shows both.assert_ne(a, b)faults if the two values are equal.assert_near(got, want, eps)faults unlessgotandwantare withinepsof each other. Use it for floats, which aren't exact.
A failing comparison tells you what it saw:
fn test_math()
assert_eq(2 + 2, 5)
end
$ scua test
FAIL ./math_test.scua test_math assert_eq failed: expected 5, got 4
0 passed, 1 failed (1 file)
#How a test fails
A test fails when it raises a fault that nothing catches: a failed
assert, a runtime fault like an out-of-range index, or an
explicit error(...). Anything that would stop a normal
program stops the test and marks it failed, with the located message. A
test that runs to the end without faulting passes.
Because each test_* runs in isolation, a fault in one is
reported and the rest keep going. If a file has a syntax, type, or
compile error, the whole file is reported as an error and its tests
count as failures.
#Files without test functions
If a *_test.scua file has no test_*
functions, the whole file is treated as a single test: it passes if it
runs without an uncaught fault. This is handy for a quick check or a
script-style test.
-- smoke_test.scua
assert(1 < 2, "basic sanity")
assert_near(0.1 + 0.2, 0.3, 0.001)
#Testing code that needs a capability
scua test cannot be handed a capability grant. A file
that says import net (or http, or
fs) is therefore unreachable from tests — including the
functions in it that only shuffle bytes and touch nothing. Splitting the
file in two gets the pure half back, but then the language has chosen
your architecture for you.
The way out is to take the capability as a parameter instead of importing it. The code under test never names the real thing, so it needs no grant, and a test hands it a stand-in you wrote by hand:
-- transport_test.scua
import str
-- The network is a parameter, not an import: `send` is fn(url) -> Ok(body) | Error(e).
-- Nothing in this file imports `http`, so `scua test` needs no grant to reach it.
fn shout_page(send, url)
let body = send(url)?
return Ok(str.upper(body))
end
fn test_body_is_shouted()
let fake = fn(u) return Ok(`hello from {u}`) end
match shout_page(fake, "example.com")
Ok(b) -> assert_eq(b, "HELLO FROM EXAMPLE.COM")
Error(e) -> assert(false, `unexpected error: {e}`)
end
end
fn test_a_refused_connection_propagates()
let broken = fn(u) return Error("connection refused") end
match shout_page(broken, "example.com")
Ok(b) -> assert(false, `expected an error, got {b}`)
Error(e) -> assert_eq(e, "connection refused")
end
end
$ scua test transport_test.scua
PASS transport_test.scua test_body_is_shouted
PASS transport_test.scua test_a_refused_connection_propagates
2 passed, 0 failed (1 file)
No grant on that command line, and the error path is as easy to test
as the happy one — a refused connection is a function that returns
Error, not a network you have to arrange.
The real capability is bound in an adapter: the one small place that names it.
import http
-- The only file that names the capability. Pure delegation, and the only part
-- a hand-written stand-in cannot replace.
fn http_transport()
return fn(url)
let resp = http.get(url)?
return Ok(resp.body)
end
end
print(shout_page(http_transport(), "http://example.com"))
Be honest about what this does and doesn't buy you. The adapter
itself stays untested — it needs the grant, so scua test
cannot reach it. That is a real gap, but it is a handful of lines of
delegation with no branching, rather than your whole library.
The pattern pays for itself a second time, which is the reason to
reach for it even where testing isn't the motive: a host
embedding SCUA can supply its own transport too. The same
parameter that takes your stand-in in a test takes the host's HTTP
client in production. A library written this way works under a host that
has no http capability to grant, because it never asked for
one.
#A note on actors
The runner shines for synchronous logic: pure functions, data
transforms, validation, anything you can call and assert on directly.
Testing a partition's message handling is harder, because a partition
processes its messages after your test function has already returned, so
you can't tell an actor and assert on the result in the
same test. For now, test the plain functions your handlers call (keep
the logic out of the handler where you can), and check end-to-end actor
behaviour by running a small program and looking at its output.