11

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?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Igor Šarčević
  • 3,481
  • 1
  • 19
  • 21

1 Answers1

17

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()));
}
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
  • 1
    Actually `write!` only needs a `write_fmt` method in scope, which is *usually* provided by `std::io::Write` or `std::fmt::Write`, but it doesn't need to be that way. – Stefan Jan 23 '18 at 09:36
  • Thanks for the detailed answer Francis. Works great. – Igor Šarčević Jan 23 '18 at 10:35
  • 2
    For future readers, the liner `fn hello(stdout: &mut io::Write) {` is usually written as `fn hello(stdout: W) {` for monomorphization (which is usually what you want). – quadrupleslap Jul 18 '18 at 10:54