Testing
Nomi tests are just Nomi code. A test declaration names a behavior and runs a
block. Put tests next to the code they exercise or in a conventional
*_test.nomi file, then run them with nomi test.
Assertions
Section titled “Assertions”Use assert for the shape you expect and refute for the shape you expect not
to hold. They work with ordinary booleans, and failed assertions produce
source-located reports.
test "arithmetic has the expected shape" {
assert 1 + 1 == 2
refute 2 * 2 == 5
}
Maybe and Result values can be asserted directly. assert checks success
shapes; refute checks failure shapes. These checks return the original value,
so payload extraction stays explicit.
fn parse_id(text: String): Result<Int, String> {
case String.to_int(text) {
Some(id) -> Ok(id)
None -> Err("expected a numeric id")
}
}
test "assert checks successful results" {
assert parse_id("42")
assert Ok(id) = parse_id("42")
assert id == 42
}
test "refute checks result errors" {
refute parse_id("wat")
assert Err(message) = parse_id("wat")
assert message == "expected a numeric id"
}
You can also assert a pattern. Pattern assertions use the same pattern language
as case arms, which is useful when the expected shape matters and the bound
names are the values you want to inspect next.
test "assert can destructure expected shapes" {
assert [left, right] = [10, 32]
assert left + right == 42
}
That same pattern form works well with enum variants because the whole expected shape is visible at the assertion site.
import std/calendar: Date, Error
test "assert can match an error variant" {
assert Err(Error.InvalidFormat(_)) = Date"not-a-date"
}
Use try when a fallible value is setup for the rest of the test. Use a
pattern assertion when the returned shape is part of what the test is checking.
import std/calendar: Date
test "formats parsed date" {
d = try Date.parse("2026-05-04")
assert Date.to_string(d) == "2026-05-04"
}
Attached Tests
Section titled “Attached Tests”Use //! directly above a declaration when the test is a compact example of
that declaration’s behavior. Each contiguous prompt group is a regular runnable
test under nomi test.
//! assert answer() == 42
//! assert (answer() + 1) == 43
//
fn answer(): Int {
42
}
//! assert label("ready") == "[ready]"
//
fn label(text: String): String {
"[" + text + "]"
}
When setup is useful, put it in the attached test before the assertion.
//! value = parse_int("42")
//! assert value == Some(42)
//
fn parse_int(text: String): Maybe<Int> {
String.to_int(text)
}
One attached test may contain shared setup and multiple assertions.
//! value = parse_int_again("42")
//! assert value == Some(42)
//! refute value == None
//
fn parse_int_again(text: String): Maybe<Int> {
String.to_int(text)
}
Checks
Section titled “Checks”Testing.check(...) uses the same assertion machinery but returns a Result
instead of propagating the failure. Reach for it when the failure itself is the
value under test. On success, the Ok contains the original subject value.
import std/testing: Testing
test "check returns assertion results as values" {
passed = Testing.check("Ada Lovelace" == "Ada Lovelace")
failed = Testing.check("Ada Lovelace" == "Grace Hopper")
assert passed == Ok(True)
refute failed
}
Pipelines
Section titled “Pipelines”assert and refute can wrap pipelines. Put the keyword at the head so the
whole pipeline reads as the assertion subject.
test "pipeline assertions validate transformed values" {
assert " ADA LOVELACE "
|> String.trim()
|> String.to_lower()
|> String.equal?("ada lovelace")
refute " GRACE HOPPER "
|> String.trim()
|> String.to_lower()
|> String.contains?("ada")
}
Groups And Setup
Section titled “Groups And Setup”Use tests to group related cases. A group can define setup, which runs
before each descendant test and returns an explicit context value. Tests opt
into the fields they need in the declaration header.
tests "user labels" {
setup {name: "Ada", role: "admin"}
test "setup context is explicit", {name, role} {
assert name == "Ada"
assert role == "admin"
}
tests "nested setup" {
setup {name, role} {name, role: "owner", active: True}
test "nested setup can extend context", {name, role, active} {
assert name == "Ada"
assert role == "owner"
assert active
}
}
}
Setup is for shared test context, not hidden globals. Ordinary bindings inside a setup expression do not leak into tests unless they are returned as part of the context value.
When code under test reads active app fields such
as MyApp.logger, use a group boot expression. It runs before setup and
stages the app value for descendant tests.
tests "app-backed behavior" { boot MyApp.load("test") test "reads active app" { assert MyApp.env == "test" } }