Question
I implemented the following Rust function and unit test:
use std::fs::File;
use std::io::Read;
use std::path::Path;
fn read_file(path: &Path) {
let mut file = File::open(path).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
println!("{contents}");
}
#[test]
fn test_read_file() {
let path = Path::new("/etc/hosts");
println!("{path:?}");
read_file(path);
}
I run the test with:
rustc --test app.rs
./app
or with:
cargo test
The test reports that it passed, but the output from println! is never shown on the screen. Why does this happen in Rust unit tests?
Short Answer
By the end of this page, you will understand that Rust’s test runner captures standard output by default, which is why println! usually does not appear for passing tests. You will also learn how to display that output, when it appears automatically, and how this behavior helps keep test results clean.
Concept
Rust’s built-in test runner captures output from stdout and stderr while tests are running. That means calls like println!() often do execute, but their text is stored instead of being shown immediately in the terminal.
This is done on purpose:
- Test output stays clean when many tests run.
- You mostly see failures, not debug noise.
- It is easier to notice which tests passed or failed.
In practice, this means:
- If a test passes, Rust usually hides the captured output.
- If a test fails, Rust usually shows the captured output for that test.
- If you want to always see output, you can disable capture.
This matters because beginners often assume println!() is broken inside tests. It is not broken—the test harness is simply intercepting the output.
Rust does this both when using:
rustc --test app.rs && ./app
and when using:
cargo test
because both use Rust’s test harness behavior for running #[test] functions.
Mental Model
Think of Rust’s test runner like a teacher collecting scratch paper during an exam.
- Your test writes messages with
println!(). - Instead of showing them immediately on the classroom board, the teacher stores them privately.
- If the test fails, the teacher reveals the notes so you can inspect what happened.
- If the test passes, the notes stay hidden to avoid clutter.
So println!() is still working—it is just being collected by the test system instead of printed directly to your terminal.
Syntax and Examples
The key idea is not new syntax, but how the test runner behaves.
Normal test output behavior
#[test]
fn demo() {
println!("This runs, but is usually hidden if the test passes");
assert_eq!(2 + 2, 4);
}
If you run:
cargo test
You will usually see only a summary such as:
running 1 test
test demo ... ok
test result: ok. 1 passed; 0 failed;
Show output explicitly
Use -- --nocapture:
cargo test -- --nocapture
Or with a compiled test binary:
rustc --test app.rs
./app --nocapture
Now the output appears:
running 1 test
This runs, but is usually hidden if the test passes
test demo ... ok
Output appears automatically on failure
Step by Step Execution
Consider this test:
#[test]
fn sample_test() {
println!("step 1");
let value = 10;
println!("step 2: {value}");
assert_eq!(value, 10);
}
Here is what happens step by step when you run cargo test:
- Rust starts the test harness.
- The test harness runs
sample_test. println!("step 1")executes.- Instead of sending that text directly to the terminal, the harness captures it.
let value = 10;stores10invalue.println!("step 2: {value}")also executes.- That output is captured too.
assert_eq!(value, 10)succeeds.- Because the test passed, the harness does not display the captured output.
- You only see the test result summary.
Now imagine the assertion changes to this:
(value, );
Real World Use Cases
This behavior is useful in real projects because test suites can contain hundreds or thousands of tests.
1. Keeping test output readable
If every passing test printed logs, the terminal would become noisy and hard to scan.
2. Debugging failing tests
Developers often temporarily add println!() statements while tracking down a bug. If a test fails, those prints help explain what happened.
3. Running CI pipelines
In automated environments like GitHub Actions, GitLab CI, or Jenkins, captured output keeps logs smaller and easier to review.
4. Testing file, network, or parsing code
When testing code that reads files, parses data, or transforms input, developers may print intermediate values during debugging without wanting those logs shown all the time.
5. Parallel test execution
Rust can run tests in parallel. If all tests printed directly to the terminal at once, the output could become interleaved and confusing. Capturing helps organize that.
Real Codebase Usage
In real Rust codebases, developers usually do not rely heavily on println!() in tests as a long-term strategy. Instead, they use it temporarily and combine it with clearer testing patterns.
Common patterns include:
Guarding behavior with assertions
Instead of printing values, tests usually assert on them directly.
#[test]
fn parses_port() {
let port = 8080;
assert_eq!(port, 8080);
}
Using println!() for temporary debugging
During debugging, developers may add prints and run:
cargo test -- --nocapture
Then remove the prints once the issue is understood.
Returning values instead of printing them
Printing is harder to test than returning data.
use std::fs::File;
use std::io::Read;
use std::path::Path;
fn read_file(path: &Path) -> std::io::Result<String> {
let = File::(path)?;
= ::();
file.(& contents)?;
(contents)
}
() {
= Path::();
= (path).();
(!contents.());
}
Common Mistakes
Mistake 1: Thinking println!() did not run
Beginners often assume this means the macro failed:
#[test]
fn my_test() {
println!("hello");
assert!(true);
}
The test passes, but nothing prints. The reason is output capture, not a broken macro.
How to avoid it: Run with:
cargo test -- --nocapture
Mistake 2: Using prints instead of assertions
Broken approach:
#[test]
fn test_value() {
let value = 42;
println!("value = {value}");
}
This does not actually verify anything.
Better:
#[test]
fn test_value() {
let value = 42;
(value, );
}
Comparisons
| Concept | What it does | When output is shown | Best use |
|---|---|---|---|
println!() in normal program execution | Prints directly to standard output | Immediately | Command-line tools, scripts, user-facing terminal output |
println!() inside Rust tests | Output is captured by the test harness | Usually only on failure, or with --nocapture | Temporary debugging during tests |
assert! / assert_eq! | Verifies expected behavior | Failure message appears when assertion fails | Real test validation |
| Returning values from functions | Makes logic testable without printing | No output unless you print it | Clean, reusable program design |
Cheat Sheet
Core rule
Rust test output is captured by default.
What happens to println!() in tests?
- It still runs
- Its output is stored by the test harness
- Passing tests usually hide that output
- Failing tests usually show it
Show output from passing tests
cargo test -- --nocapture
or:
rustc --test app.rs
./app --nocapture
Best practice
Prefer this:
assert_eq!(actual, expected);
over this:
println!("{actual}");
Better testable design
Instead of printing inside a function:
fn read_file(path: &Path) {
// prints content
}
prefer returning a value:
(path: &Path) std::io::<> {
}
FAQ
Why is println! hidden in Rust tests?
Because Rust’s test harness captures standard output by default to keep test results clean.
How do I force println! to show during tests?
Run:
cargo test -- --nocapture
Does println! run at all in a passing test?
Yes. The output is just captured instead of displayed.
Why does output sometimes appear when a test fails?
Rust usually prints captured output for failing tests to help with debugging.
Is this behavior the same for cargo test and rustc --test?
Yes. Both use Rust’s test harness behavior.
Should I use println! for testing logic?
Usually no. Prefer assertions like assert_eq! and assert!.
Why is returning a value better than printing it in tests?
Returned values are easier to inspect, compare, and assert on, which makes tests more reliable.
Why is /etc/hosts not ideal in a unit test?
Mini Project
Description
Build a small Rust test example that demonstrates how output capture works. The project will contain one passing test and one failing test, both using println!(), so you can see when Rust hides output and when it reveals it. This helps you understand the difference between normal execution and test execution.
Goal
Create a Rust test file that proves println!() output is captured by default and can be shown with --nocapture.
Requirements
- Create one passing test that uses
println!(). - Create one failing test that uses
println!(). - Add a small function that returns a value so you can assert on it.
- Run the tests normally and then run them again with
--nocapture. - Observe how output differs between passing and failing tests.
Keep learning
Related questions
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Default Function Arguments in Rust: What to Use Instead
Learn how Rust handles default function arguments, why they are not supported, and practical patterns to achieve similar behavior.
Fixing Rust "linker 'cc' not found" on Debian in WSL
Learn why Rust shows "linker 'cc' not found" on Debian in WSL and how to fix it by installing the required C build tools.