How to make a unit test in rust for a function that uses console input (io::stdin)?

hi, I'm new to Rust, I've been trying to find a solution to my problem on the internet, but everywhere (example) they recommend passing an option to a function, but I need the function to not receive anything

here is an example function

fn yes_or_no(prompt: &str) -> bool {
    let approvals_array = ["y", "yes", "т", "так", "д", "да"];

    print!("{} ", prompt);
    std::io::stdout().flush().unwrap();

    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    let input = input.trim().to_lowercase();

    return approvals_array.contains(&input.as_str());
}

please, help

The solution for testing functions that perform I/O is to avoid hardcoding the source to read from. Instead of making the function write to/read from stdout/stdin, make it take a Read and Write instead:

use std::io;

fn yes_or_no<R, W>(prompt: &str, mut reader: R, mut writer: W) -> io::Result<bool>
where
    R: io::BufRead,
    W: io::Write,
{
    let approvals_array = ["y", "yes", "т", "так", "д", "да"];

    write!(writer, "{} ", prompt);
    writer.flush()?;

    let mut input = String::new();
    reader.read_line(&mut input)?;
    let input = input.trim().to_lowercase();

    Ok(approvals_array.contains(&input.as_str()))
}

Then you can test it using e.g. io::Cursor and/or Vec: Playground

6 Likes

Is it possible to not pass mut reader: R, mut writer: W to the function, because then in all places where this function will be used, io::stdout() will have to be fussed, I think it will be redundant (unnecessary) because it will be everywhere standard input ?

No. But you can wrap it in another convenience function that always passes stdin/stdout, and use that in your code instead.

By the way, if this is a function always purely used for CLI, you probably shouldn't bother unit testing it. It will be obvious when it breaks.

2 Likes

I'm trying to develop an application in a group to make sure that they understand correctly what behavior of the function I expect, I wanted to write unit tests that would reflect the nuances of behavior depending on certain input data, maybe there is a solution?

I've used this crate for testing an app processes stdin appropriately.
assert_cmd crate
You can build up unit tests in tests/cli.rs and run with cargo test.
This may be of use.

2 Likes

assert_cmd works if there is only one prompt for user input. If you have series of prompts then H2CO3's suggestion is the way to go.

Dear @H2CO3, thanks for the suggestion. When I tried using the yes_or_no() in the main I got the error:

Compiling upcase v0.1.0 (C:\Users\manukyae\00-work\stdin-buff)
error[E0277]: the trait bound `Stdin: BufRead` is not satisfied
  --> src\main.rs:4:29
   |
4  |     yes_or_no("yes or no?", &mut io::stdin(), &...   |     ---------               ^^^^^^^^^^^^^^^^ the trait `BufRead` is not implemented for `Stdin`
   |     |
   |     required by a bound introduced by this call
   |

The full code I used:

main.rs

use std::io;

fn main() -> io::Result<()> {
    yes_or_no("yes or no?", &mut io::stdin(), &mut io::stdout())?;
    Ok(())
}

fn yes_or_no<R, W>(prompt: &str, mut reader: R, mut writer: W) -> io::Result<bool>
where
    R: io::BufRead,
    W: io::Write,
{
    let approvals_array = ["y", "yes", "т", "так", "д", "да"];

    write!(writer, "{} ", prompt)?;
    writer.flush()?;

    let mut input = String::new();
    reader.read_line(&mut input)?;
    let input = input.trim().to_lowercase();

    Ok(approvals_array.contains(&input.as_str()))
}

You'd have to lock stdin.

1 Like

Brilliant, thanks so much. This works like a charm:

fn main() -> io::Result<()> {
    yes_or_no("yes or no?", io::stdin().lock(), io::stdout())?;
    Ok(())
}

Can't you drop the &muts? Both return values should be Read/Write (respectively), without the reference.

1 Like

Correct, plain io::stdin().lock(), io::stdout() do the job! Thanks! Modified the code above to reflect this.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.