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.