Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing standard output by substituting stdout [duplicate]

Tags:

rust

My goal is to test the output of a function that goes to the standard output. So far, my best attempt is to substitute the stream with a string in the tests.

This is what I managed to achieve so far:

use std::io;
use std::fmt;

fn hello(stdout: &mut std::fmt::Write) {
    writeln!(stdout, "Hello world");
}

#[test]
fn hello_test() {
    let mut stdout = String::new();

    // pass fake stdout when calling when testing
    hello(&mut stdout);

    assert_eq!(stdout, "Hello world\n".to_string());
}

fn main() {
    // pass real stdout when calling from main

    hello(&mut io::stdout());
}

The tests work, but unfortunately io::stdout() does not implement the fmt::Write trait.

What is the best solution for testing a function that writes to the standard output in Rust? Is there a way to fix my solution with strings, or should I look for an alternative?

like image 349
Igor Šarčević Avatar asked Jan 23 '18 00:01

Igor Šarčević


1 Answers

The write! macro expects the destination operand to implement either std::fmt::Write or std::io::Write. Since writeln! delegates to write!, this also applies to writeln!.

The documentation for std::fmt::Write says this:

the io::Write trait is favored over implementing this trait

Since Stdout implements std::io::Write, you should change the bounds on your code from fmt::Write to io::Write. Note, however, that String doesn't implement io::Write, since io::Write accepts arbitrary bytes that may not be well-formed UTF-8; you can use Vec<u8> instead.

use std::io;

fn hello(stdout: &mut io::Write) {
    writeln!(stdout, "Hello world");
}

#[test]
fn hello_test() {
    let mut stdout = Vec::new();

    // pass fake stdout when calling when testing
    hello(&mut stdout);

    assert_eq!(stdout, b"Hello world\n");
}

fn main() {
    // pass real stdout when calling from main

    hello(&mut io::stdout());
}

For improved performance, if only one threads need to write on stdout, consider passing a StdoutLock rather than a Stdout to your function (with Stdout, each write acquires and releases a lock).


If you really prefer to use std::fmt::Write instead, then you could use an adapter struct that converts fmt::Write calls to io::Write calls.

use std::io;
use std::fmt;

struct WriteAdapter<W>(W);

impl<W> fmt::Write for WriteAdapter<W>
where
    W: io::Write,
{
    fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
        self.0.write_all(s.as_bytes()).map_err(|_| fmt::Error)
    }

    fn write_fmt(&mut self, args: fmt::Arguments) -> Result<(), fmt::Error> {
        self.0.write_fmt(args).map_err(|_| fmt::Error)
    }
}

fn hello(stdout: &mut fmt::Write) {
    writeln!(stdout, "Hello world");
}

#[test]
fn hello_test() {
    let mut stdout = String::new();

    // pass fake stdout when calling when testing
    hello(&mut stdout);

    assert_eq!(stdout, "Hello world\n");
}

fn main() {
    // pass real stdout when calling from main

    hello(&mut WriteAdapter(io::stdout()));
}
like image 132
Francis Gagné Avatar answered Nov 02 '22 09:11

Francis Gagné