I am writing a CLI question asking library for my first Rust project since I will probably be using it anyway, and I cannot find a clean way to test the terminal
method of the builder pattern, which using the configuration gets user input and returns an answer.
pub fn confirm(&mut self) -> Answer {
self.yes_no();
self.build_prompt();
let prompt = self.prompt.clone();
let valid_responses = self.valid_responses.clone().unwrap();
loop {
let stdio = io::stdin();
let input = stdio.lock();
let output = io::stdout();
if let Ok(response) = prompt_user(input, output, &prompt) {
for key in valid_responses.keys() {
if *response.trim().to_lowercase() == *key {
return valid_responses.get(key).unwrap().clone();
}
}
self.build_clarification();
}
}
}
Looking for a solution I discovered dependency injection which allowed me to write tests for the function that prompts the user for input using Cursor
. It does not let me change the user input to the confirm()
function for each test of Question::new("Continue?").confirm()
though so I tried using conditional compilation, and came up with the following.
#[cfg(not(test))]
fn prompt_user<R, W>(mut reader: R, mut writer: W, question: &str) -> Result<String, std::io::Error>
where
R: BufRead,
W: Write,
{
write!(&mut writer, "{}", question)?;
let mut s = String::new();
reader.read_line(&mut s)?;
Ok(s)
}
#[cfg(test)]
fn prompt_user<R, W>(mut reader: R, mut writer: W, question: &str) -> Result<String, std::io::Error>
where
R: BufRead,
W: Write,
{
use tests;
Ok(unsafe { tests::test_response.to_string() })
}
And in the tests
module I use a global variable:
pub static mut test_response: &str = "";
#[test]
fn simple_confirm() {
unsafe { test_response = "y" };
let answer = Question::new("Continue?").confirm();
assert_eq!(Answer::YES, answer);
}
This works as long as I only run tests with a single thread, but also no longer allows me to test the real user input function. Not really a problem for such a small crate but it is very messy. I did not see any solutions to do this from any available testing libraries.
As mentioned in the Stack Overflow question you linked, you should generally avoid hard-wiring external dependencies (a.k.a. I/O) if you want testability:
In all such cases, I recommend using Dependency Injection:
Then, when writing:
Finally, instantiate the production dependencies in main, and forward them from there.
Tricks, not treats:
Environment
structure which contains all such interfaces, rather than passing heaps of arguments to each function; however functions which only require one/two resource(s) should take those explicitly to make it clear what they use,now()
may return different results as the time passes.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With