Writing tests
Dune has tests built into the language. Tests can be written as a named test
block or as a zero-argument unit function marked with
@test. The dune test
command discovers both forms and reports the results.
A first test
from assert import assert_eq;
fn double(x: int): int {
return x * 2;
}
test "double multiplies by two" {
assert_eq(double(2), 4);
assert_eq(double(0), 0);
}
Run it with:
dune test path/to/file.dn
running 1 test
test "double multiplies by two" ... ok
test result: ok. 1 passed; 0 failed
The name after test is an ordinary string literal, so it can contain spaces
and punctuation. The body is a normal block: it can declare bindings, call
functions, and loop, just like a function body.
The function form is useful when a test should also have an ordinary declaration name:
@test
fn double_handles_zero(): unit {
assert_eq(double(0), 0);
}
An attributed test function must take no parameters, explicitly return unit,
and cannot be generic, foreign, or foreknown.
Use @ignore to keep a temporarily disabled test discoverable, optionally with
a reason:
@test
@ignore("requires a local database")
fn database_round_trip(): unit { }
Use @should_panic when failure is the behavior under test. An optional text
argument requires the panic report to contain that text:
import runtime;
@test
@should_panic("index out of bounds")
fn rejects_invalid_index(): unit {
runtime.panic("index out of bounds");
}
An expected-panic test fails if it returns normally or produces a different
message. Use @should_fail("message") instead when any typed runtime failure,
such as a bounds or arithmetic error, is acceptable. @ignore,
@should_panic, and @should_fail are mutually exclusive.
Assertions
The assert module provides the helpers that fail a
test. Each one calls runtime.panic when its check does not hold, which aborts
the current test and marks it as failed:
from assert import assert_eq, assert_true, assert_false;
test "assertions" {
assert_eq(1 + 1, 2); // any comparable type
assert_true(1 < 2);
assert_false(2 < 1);
}
assert_eq<T> works for any type whose values can be compared with ==, so it
handles integers, reals, text, and booleans alike. Import the helpers with
from assert import ... to call them unqualified, or import assert; and write
assert.assert_eq(...).
How tests run
- Isolation.
dune testruns only attributed test functions andtestblocks. A file's top-level code (statements outside any function or test) does not run, so a script and its tests can live in the same file. - Shared declarations. Top-level functions, constants, records, and imports are all in scope inside every test, so tests exercise the same code the rest of the file uses.
- Independent failures. A failing assertion aborts only the test it is in. The remaining tests still run, and the final line summarises how many passed and failed.
- Exit code.
dune testexits non-zero if any test fails, which lets it gate a CI pipeline. - Ignored tests.
@ignoretests are not executed and are reported in a separate ignored count; they do not make the command fail.
A failing run looks like this:
running 2 tests
test "this assertion holds" ... ok
test "this assertion fails" ... FAILED
panic: assertion failed: values are not equal
stack trace:
0: assert.assert_eq
at stdlib/assert.dn:54:9
1: test "this assertion fails"
at tests/example.dn:8:5
test result: FAILED. 1 passed; 1 failed
The named test "..." block is a real outer stack frame, so failures retain
the assertion helper, user functions, imported module files, and the exact line
inside the test. See Runtime errors and stack traces.
Rules
testblocks and@testfunctions are only allowed at the top level of a file. A test inside a function or another block is a compile-time error.testblocks are ignored when a file is run normally. An@testfunction is not run automatically, but remains callable like any other named function.