rustc --test: build a test runner from a single Rust file
- tool
- Rust
- command
- rustc
- flag
- --test
- default
- off (rustc builds the crate's normal binary or library)
rustc --test file.rs compiles the file into a test runner instead of a normal program: code under #[cfg(test)] is included, every #[test] function is registered, and rustc generates a main that runs them with the standard libtest harness. Running the resulting binary prints the familiar running N tests ... test result: report and exits with 101 if any test fails. cargo test does exactly this for each crate, plus integration tests and doc tests.
Build and run
cat > calc.rs <<'EOF'
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds() { assert_eq!(add(2, 2), 4); }
#[test]
fn fails() { assert_eq!(add(2, 2), 5); }
#[test]
#[ignore]
fn slow() {}
}
fn main() { println!("main: {}", add(1, 2)); }
EOF
clean() { sed -E "s/^thread '([^']+)' \([0-9]+\)/thread '\1'/; s/finished in [0-9.]+s/finished in <time>/"; }
rustc --test calc.rs -o calc-tests
./calc-tests --test-threads=1 2>&1 | clean; echo "exit: ${PIPESTATUS[0]}"
echo "== one test by name"
./calc-tests adds 2>&1 | clean
echo "== list"
./calc-tests --list
echo "== normal build"
rustc calc.rs -o calc && ./calcOutput
running 3 tests
test tests::adds ... ok
test tests::fails ... FAILED
test tests::slow ... ignored
failures:
---- tests::fails stdout ----
thread 'tests::fails' panicked at calc.rs:9:18:
assertion `left == right` failed
left: 4
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::fails
test result: FAILED. 1 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in <time>
exit: 101
== one test by name
running 1 test
test tests::adds ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in <time>
== list
tests::adds: test
tests::fails: test
tests::slow: test
3 tests, 0 benchmarks
== normal build
main: 3The failing assertion shows the left and right values; the ignored test is reported but not run. A name given to the test binary is a filter (substring match), and --list prints the registered tests. Without --test the same file builds the regular program, whose main runs and the tests are left out. The clean function only replaces the run time and the thread number, which change on every run.
Options of the test binary
| Option | Effect |
|---|---|
| NAME | Run only tests whose path contains NAME (several filters allowed) |
| --exact | Filters must match the full test path, e.g. tests::adds |
| --ignored / --include-ignored | Run only the #[ignore] tests / run them as well |
| --test-threads=N | Number of tests run in parallel; 1 gives a stable order |
| --nocapture | Do not capture stdout and stderr of tests |
| --show-output | Print the captured output of passing tests too |
| --list | List tests without running them |
| --skip NAME | Skip tests whose path contains NAME |
These are arguments of the generated binary, not of rustc. With Cargo they go after --: cargo test -- --test-threads=1 --nocapture, and a filter can go before it (cargo test adds, see run a single test).
Related rustc flags
--cfg testwithout--testcompiles the#[cfg(test)]code but no harness, which is rarely what you want.-C opt-level=3 --testbuilds optimized tests, the equivalent ofcargo test --release; overflow checks are then off (see rustc -C opt-level).--crate-type lib --testworks the same way for library files that have nomain.
Pitfalls
- A file that only contains a library (no
main) builds fine with--test, but without--testrustc expects a binary and fails with error E0601 (mainfunction not found in crate); pass--crate-type libin that case. - Tests run in parallel by default, so their output can interleave and tests sharing a file or environment variable can interfere. Use
--test-threads=1while you debug. - Exit code 101 is the Rust panic code; CI treats any non-zero code as failure, so no extra flag is needed.
FAQ
Why does rustc --test not run my main function?
The test harness provides its own main, which runs the #[test] functions. Your main is compiled but not called. Build without --test to get the normal program.
How do I see println! output from a passing test?
Pass --nocapture to the test binary (./tests --nocapture), or --show-output to print captured output of passing tests after the run. With Cargo, put the options after a double dash: cargo test -- --nocapture.
Can I run the tests of a single file without Cargo?
Yes, that is what rustc --test file.rs is for: it needs no Cargo.toml. As soon as the code uses crates from crates.io, switch to Cargo, which passes the --extern flags for you.