3
\$\begingroup\$

I am interested in any kind of possible improvement to this method of testing a Rust program with user interaction in an end-to-end manner (simulating user input and asserting a certain program output given a certain user input), I also added an example usage of this framework with a number guessing game for clarity.

This method works by passing BufRead and Write functions to the functions, that can be either be used with the std::io::stdin() and std::io::stdout() values in the normal program or mocked when testing. This testing method works but complicates the arguments and the usage of the function to be tested. Is there any other method that can be applied without making changes to the function to be tested?

use std::io::{BufRead, Write, Cursor};
use rand::Rng;

fn play_game<R: BufRead, W: Write>(input: &mut R, output: &mut W, num: u32) -> std::io::Result<()> {
    writeln!(output, "Guess the number!")?;
    writeln!(output, "Please input your guess.")?;

    let secret_number = num;

    loop {
        let mut guess = String::new();
        input.read_line(&mut guess)?;
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                writeln!(output, "Invalid input. Please enter a number.")?;
                continue;
            }
        };

        writeln!(output, "You guessed: {guess}")?;

        match guess.cmp(&secret_number) {
            std::cmp::Ordering::Less => writeln!(output, "Too small!")?,
            std::cmp::Ordering::Greater => writeln!(output, "Too big!")?,
            std::cmp::Ordering::Equal => {
                writeln!(output, "You win!")?;
                break;
            }
        }

        writeln!(output, "Please input your guess.")?;
    }

    Ok(())
}

fn main() -> std::io::Result<()> {
    let stdin = std::io::stdin();
    let mut stdout = std::io::stdout();

    let secret_number = rand::thread_rng().gen_range(1..101);

    play_game(&mut stdin.lock(), &mut stdout, secret_number)?;

    Ok(())
}
#[test]
fn test_play_game() {
    let input = b"50\n75\n60\n55\n52\n53";
    let expected_output = "Guess the number!\n\
                       Please input your guess.\n\
                       You guessed: 50\n\
                       Too small!\n\
                       Please input your guess.\n\
                       You guessed: 75\n\
                       Too big!\n\
                       Please input your guess.\n\
                       You guessed: 60\n\
                       Too big!\n\
                       Please input your guess.\n\
                       You guessed: 55\n\
                       Too big!\n\
                       Please input your guess.\n\
                       You guessed: 52\n\
                       Too small!\n\
                       Please input your guess.\n\
                       You guessed: 53\n\
                       You win!\n";


    let mut input_stream = Cursor::new(input);
    let mut output_stream = Cursor::new(Vec::new());

    play_game(&mut input_stream, &mut output_stream, 53).unwrap();

    let actual_output = String::from_utf8(output_stream.into_inner()).unwrap();

    assert_eq!(actual_output, expected_output);
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

What you probably want to do is move from unit test to integration tests. That is, test your binary as a whole.

Put your tests in tests/*.rs. You can use the assert_cmd crate (or see how it works) to figure out how to get a path to the binary of your application. Spawn the binary and communicate with it over the stdin and stdout. The rexpect crate might be useful here.

\$\endgroup\$
4
  • \$\begingroup\$ Thanks for your feedback Winston, could you please include an example of how your method could test my number guessing game and if/how much the number guessing game function could be simplified when using this new method? It would be useful not only for myself but for all future visitors. \$\endgroup\$
    – Caridorc
    Commented Apr 10, 2023 at 0:48
  • \$\begingroup\$ @Caridorc, no, Im leaving that to you \$\endgroup\$ Commented Apr 10, 2023 at 2:31
  • \$\begingroup\$ @WinstonEwert Bare in mind that testing the binary from outside does not give you the option to specify the secret number which may lead you to a complex nondeterministic test unless you can somehow mock the rng. \$\endgroup\$
    – slepic
    Commented Apr 10, 2023 at 6:40
  • \$\begingroup\$ @slepic Good point \$\endgroup\$ Commented Apr 10, 2023 at 14:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.