Question

Why `println!` Doesn’t Show in Rust Unit Tests

rusttestingprintln

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.

Take Quiz

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

#[test]
fn demo_failure() {
    println!("About to fail");
    assert_eq!(1, 2);
}

When this test fails, Rust usually prints the captured output to help debugging.

Your example with a small improvement

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);
}

To see both println! calls:

cargo test -- --nocapture

The code itself is fine. The hidden output is caused by the test runner, not by println!.

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:

  1. Rust starts the test harness.
  2. The test harness runs sample_test.
  3. println!("step 1") executes.
  4. Instead of sending that text directly to the terminal, the harness captures it.
  5. let value = 10; stores 10 in value.
  6. println!("step 2: {value}") also executes.
  7. That output is captured too.
  8. assert_eq!(value, 10) succeeds.
  9. Because the test passed, the harness does not display the captured output.
  10. You only see the test result summary.

Now imagine the assertion changes to this:

assert_eq!(value, 99);

Then the steps are similar, but at the end:

  • The assertion fails.
  • The test harness reports the failure.
  • The captured output is usually shown so you can inspect what the test printed.

That is why println! feels invisible in passing tests but suddenly appears when a test fails.

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.

Take Quiz

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 mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

#[test]
fn test_read_file() {
    let path = Path::new("/etc/hosts");
    let contents = read_file(path).unwrap();
    assert!(!contents.is_empty());
}

This is more common in production code because:

  • returned values are easier to verify
  • tests become more deterministic
  • output formatting is separated from logic

Error handling instead of unwrap() everywhere

Real code often uses Result to make tests clearer and safer.

#[test]
fn test_read_file() -> std::io::Result<()> {
    let path = Path::new("/etc/hosts");
    let contents = read_file(path)?;
    assert!(contents.contains("localhost"));
    Ok(())
}

Logging instead of raw printing

In larger codebases, developers may use logging crates for debug information, but the test harness capture behavior still matters because terminal output is still managed during test runs.

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;
    assert_eq!(value, 42);
}

Mistake 3: Testing by reading a system file like /etc/hosts

This may work on many Unix-like systems, but it is not portable across all environments.

let path = Path::new("/etc/hosts");

Problem:

  • May fail on Windows
  • Depends on machine setup
  • Makes tests less predictable

How to avoid it: Create a temporary test file or use fixture data under your project.

Mistake 4: Overusing unwrap() in examples without understanding failures

let mut file = File::open(path).unwrap();

This is fine for quick experiments, but if the file cannot be opened, the test will panic.

How to avoid it: Use Result where possible and make the failure clearer.

Mistake 5: Forgetting that tests may run in parallel

If several tests all print output, logs can be harder to interpret when uncaptured.

How to avoid it:

  • Keep output minimal
  • Use targeted test runs
  • Prefer assertions over manual print debugging

Comparisons

ConceptWhat it doesWhen output is shownBest use
println!() in normal program executionPrints directly to standard outputImmediatelyCommand-line tools, scripts, user-facing terminal output
println!() inside Rust testsOutput is captured by the test harnessUsually only on failure, or with --nocaptureTemporary debugging during tests
assert! / assert_eq!Verifies expected behaviorFailure message appears when assertion failsReal test validation
Returning values from functionsMakes logic testable without printingNo output unless you print itClean, reusable program design

cargo test vs cargo test -- --nocapture

CommandBehavior
cargo testRuns tests and hides output from passing tests
cargo test -- --nocaptureRuns tests and shows println!() output

Printing vs asserting

ApproachGood forWeakness
Printing valuesQuick debuggingDoes not verify correctness
AssertionsReliable testsLess useful for inspecting many intermediate states
Returning values and asserting themClean design and testabilityRequires slightly better function structure
Take Quiz

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:

fn read_file(path: &Path) -> std::io::Result<String> {
    // returns content
}

Useful reminders

  • println!() is for output
  • assert!() is for testing
  • --nocapture is for debugging test output
  • Passing tests normally stay quiet

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?

It depends on the operating system and environment, so the test may not be portable or predictable.

Take Quiz

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

  1. Create one passing test that uses println!().
  2. Create one failing test that uses println!().
  3. Add a small function that returns a value so you can assert on it.
  4. Run the tests normally and then run them again with --nocapture.
  5. Observe how output differs between passing and failing tests.
Take Quiz

Keep learning

Related questions

View more