Run dodo test in your project folder. It finds tests recursively, compiles
them, and reports each result. No manifest, test dependency, or main function
is needed. Like dodo run, it needs a C toolchain for linking.
Write your first test
Save this as arithmetic.dodo:
package arithmetic
fn add(a: i32, b: i32) -> i32 {
a + b
}
@test
fn adds_numbers() {
assert_eq(add(20, 22), 42)
assert(add(1, 2) > 0, "positive inputs should produce a positive sum")
}
Run it from the same folder:
dodo test
The output includes arithmetic.dodo::adds_numbers ... ok and a summary of
passed, failed, ignored, and filtered tests. A failing assertion reports the
source file, line, column, expression, and comparison values.
Temporarily change the expected 42 to 41 and run the test again. It should
fail at that assertion. Restore 42 afterward. This checks that you are running
the intended file and that the assertion can detect a mistake.
Test the behavior a caller depends on: a normal input, a boundary such as an empty slice, and a recoverable error. Keep calculations in functions separate from console, files, or sockets when practical, so most tests need no external setup. The temperature-report walkthrough shows that separation in a complete application.
Choose where tests live
Any .dodo file can contain @test functions. Functions named test_... are
also discovered without an attribute. Tests must be safe, top-level functions
with no generic parameters, no arguments, and a void return type. Use an
unsafe block inside a test when the operation needs one.
An inline test loads its source file and imports, just like an ordinary source
file passed to dodo run. It can call private helpers in that file. For tests
in a separate companion file, name it arithmetic_test.dodo or
arithmetic.test.dodo. Companion tests load all immediate .dodo files in
their folder as one package, so they can call private helpers in adjacent
files. All those files must declare the same package. The test runner does
not call the application’s main function.
For a separate integration suite, use ordinary imports to exercise public package APIs. Import paths resolve relative to the test source, following the same rules as other Dodo programs.
Directory discovery skips hidden files and folders, build, target, dist,
node_modules, vendor, and symlinks. Ordinary application entry points and
unmarked Markdown snippets are not executed. Each test is identified by its
source path and function name, and imported tests are not executed a second
time through their importer. Files and tests have deterministic order: paths
are sorted, and functions follow source order.
Assert behavior
Assertions are available without imports:
| Call | Succeeds when |
|---|---|
assert(condition) |
The Boolean condition is true. |
assert_eq(left, right) |
The two values are equal. |
assert_ne(left, right) |
The two values are different. |
Each call accepts an optional final &str message. Arguments are evaluated
once, from left to right, including the message. Equality assertions accept
matching numeric types, Booleans, raw pointers, fieldless enums, and &str.
Strings compare their bytes, including embedded NULs. Numbers use the language’s
normal equality rules; use an explicit tolerance when testing approximate
floating-point calculations. Compare array elements or structure fields
explicitly.
package assertions
fn test_values() {
assert_eq(255u8, 255, "the literal uses the other operand's type")
assert_ne("ready", "waiting")
assert_eq("héllo", "héllo")
}
These assertions also work in ordinary programs, where failure follows the
selected runtime failure policy
without adding a hosted test runtime. Checks remain enabled at every optimization
level. Use core.assert, core.assert_eq, or core.assert_ne when a local
function has the same name. Test functions remain ordinary type-checked Dodo
functions during check and compile; attributes do not hide invalid code.
Find and select tests
dodo test --list
dodo test arithmetic.dodo
dodo test path/to/project
dodo test --filter adds
dodo test --exact --filter arithmetic.dodo::adds_numbers
dodo test --skip slow
dodo test -O 3
--filter matches a substring of the path or function name. Repeat it to match
any of several filters. --exact matches the whole function name or the full
path::name identifier printed by --list. Repeat --skip to exclude several
substrings. --list parses and lists tests without resolving imports, checking
their bodies, or invoking a linker.
An empty suite or a filter matching no tests exits with status 1 and a discovery
hint. Use --allow-empty when an empty suite is expected. This keeps misspelled
filters from silently passing in CI.
Ignore a test with a reason
package devices
@test
@ignore("requires a connected device")
fn device_round_trip() {
assert(true)
}
Ignored tests are listed with their reason and skipped by default. Run only
ignored tests with --ignored, or all selected tests with --include-ignored.
Ignored functions in a compiled source still need to type-check.
Understand failures
Each test runs in a fresh native process. An assertion failure, checked runtime trap, nonzero exit, or signal fails that test; remaining tests continue. Compiler-generated traps report the checked expression’s location, including inside imported helpers. A raw native crash reports the process status and test declaration location. Tests do not unwind or run destructors after a trap.
Standard output and standard error are captured separately and shown for failed
tests. Add --show-output to see successful tests’ output too. Reports show up
to 64 KiB per stream, with a truncation notice for longer output. A test has
30 seconds to execute by default; compilation time is separate.
dodo test --show-output
dodo test --timeout 5
dodo test --timeout 0
dodo test --fail-fast
--timeout 0 disables the deadline. Tests run sequentially with the caller’s
working directory and environment, and with standard input closed. Relative
fixture paths therefore resolve from the folder where you invoke the command.
Process isolation resets memory between tests; files and other external state
are shared. On Unix, the runner also terminates each test’s process group when
the case ends. Temporary executables and captured output are removed afterwards.
The final exit status is 0 when the selected suite succeeds, and 1 for test
failures, discovery errors, build errors, or an empty selection. --linker,
DODO_CC, and repeatable --link-arg options work as with compile. Tests run
on the host platform.
Execute documentation examples
Mark complete, runnable Markdown examples with dodo test on the opening
fence. They participate in normal discovery; --doc selects documentation
only, and --no-doc selects source tests only.
```dodo test
package example
fn main() {
assert_eq(6 * 7, 42)
}
```
Each marked fence is an independent Dodo source file, with its own package
declaration and imports. It needs fn main(), fn main() -> i32, or test
functions. A documentation main passes when it returns normally with status
0. Test functions within a fence run independently. Imports resolve beside the
Markdown file, and failures point to the original document’s line and column.
Backtick and tilde fences are supported in .md and .mdx files.
Keep incomplete, illustrative, or environment-dependent snippets marked simply
dodo. Only explicitly marked examples execute. The examples in this guide
are executable and checked in compiler CI.
dodo test docs --doc
Test this repository
For your own project’s CI, format source once locally, then check formatting without rewriting files and run tests:
dodo fmt --check .
dodo check main.dodo
dodo test
Run check on your actual entry file; a library-only project need not have
main.dodo. A --list result proves discovery only, not compilation or execution.
Ensure the CI machine also has the linker prerequisites.
Use --allow-empty only when having no tests is intentional.
From a source checkout, build the compiler and run the same command application authors use:
cargo build --locked
target/debug/dodo test
target/debug/dodo test -O 3
target/debug/dodo test examples
target/debug/dodo test tests/stdlib --filter math
The native suite includes language example boundaries, allocation and ownership, collections, byte/text processing, formatting, memory I/O, hashes, numerical reference vectors, fake clocks, and in-memory network/HTTP/web protocols. Existing self-contained fixtures expose test entry points; larger core, math, and time fixtures provide separate tests for each independent group. Generated MPFR vectors have one test per mathematical function, and their generator keeps those entry points in sync.
The Rust suite remains responsible for compiler rejection tests, cross-target
emission, and integration fixtures needing external peers or platform setup.
Run it with cargo test --locked --all-targets. CI runs both suites, including
the native suite at optimization levels 0 and 3.
Save test settings
An optional project manifest can
save a discovery root, timeout, and hosted compiler settings. dodo test -g
emits debug information; --release and --profile NAME select profiles.
Tests use their own [test.build] settings, so an application target configured
for firmware does not change the hosted test runner. Use --print-config to
inspect effective settings without discovery or execution.