Tests
Tests are part of the language basics because they use ordinary functions,
bindings, control flow, and error propagation. A test declaration has a
name, no parameters, and a Unit body:
func add(left : Int, right : Int) -> Int do left + rightend
test "adds two integers" do Testing.assert_equal(add(20, 22), 42)!;endRun the tests declared by a package directory with:
noodle test .The test command checks the package, builds an isolated test runner, and runs the target package’s tests in module order. Tests in dependency packages are not copied into the target package’s test plan. A test can refer to private declarations in its own module, which makes it useful for checking implementation details without exporting them.
Assertions and failure propagation
Section titled “Assertions and failure propagation”The standard assertion helpers are:
Testing.assert(condition)!for a boolean condition;Testing.assert_equal(actual, expected)!for a value comparison; andTesting.fail(message)!for an unconditional failure.
The postfix ! propagates the assertion’s Result failure to the test
runner. A test does not declare throws; the compiler infers the error values
that can leave the test body. You can use the same form when checking another
fallible function:
errortype ParseError InvalidInputend
func parse_count(text : String) throws ParseError -> Int do _ = text; 42end
test "accepts a count" do Testing.assert_equal(parse_count("42")!, 42)!;endAssertion failures include rendered actual and expected values where
applicable, together with source locations autofilled from the expressions in
the assertion. The Testing API and its debug rendering are covered in
Output, debugging, and tests.
Await tasks in an async test
Section titled “Await tasks in an async test”Prefix a test with async when its body needs to await a task. The test runner
waits for the returned task before deciding whether the test passed. Assertions
and propagated business errors use the same failure reporting as synchronous
tests:
async func load_count() -> Int do 42end
async test "awaits a task" do count = load_count().await()!; Testing.assert_equal(count, 42)!;endAn ordinary test cannot await a task; use async test even when the awaited
function has no business errors. Async tests still run serially in the package
test plan and use the ordinary or @expensive timeout selected by the runner.
Mark intentionally slow tests
Section titled “Mark intentionally slow tests”Use the argument-free @expensive attribute only when a test is inherently
expected to need a larger time budget, such as a broad integration test or a
large generated fixture:
func build_fixture() -> Array[Int] do [1, 2, 3]end
@expensivetest "checks the complete generated fixture" do expected = [1, 2, 3]; Testing.assert_equal(build_fixture(), expected)!;end@expensive does not skip the test and does not change its semantics. It tells
the current runner to use its separate expensive-test timeout; ordinary tests
default to 200 milliseconds and expensive tests default to 15,000 milliseconds.
The CLI can override those budgets with --timeout and
--expensive-timeout.
For a shorter feedback loop, quick mode still checks and compiles every test
but skips the execution of tests marked @expensive:
noodle test --quick .The final summary reports how many tests were skipped.
Do not add @expensive merely to hide a hang or a performance regression.
Warning when an expensive test consistently completes unusually quickly is
tracked as a CLI follow-up rather than current behavior.
Keep tests close to their code
Section titled “Keep tests close to their code”Tests may live at the end of the primary .nl file. For a large module, put
them in an explicitly included *.tests.part file so they share the module’s
private scope without creating a second module. See
Modules and packages for source parts and
their last-resort role in source organization.
Next: Modules and packages.