I'm new to Rust coming from C. I was doing the excellent CLI tutorial,
and as one of the exercises suggest, I changed the implementation of the grep
-like tool to use buffered I/O.
But when it came the time to test the function, I saw that the API I created was not testable, as the ownership of one
parameter is transferred to the function under test, and therefore I cannot assert the desired property from it once
the function returns.
I was wondering if experienced Rustaceans would suggest me a crate or technique that would allow one to
assert properties of a function under test before the local variables would go out of scope?
Maybe something like aspect-oriented programming, that would allow one to "weave" the test assertions in the scope of
the function under test?
The best I could think of was having a generic public API that accepts the any Trait
implementing entity, then wraps
it into the buffers, and passes the buffers as mutable references to an inner function, and this inner function gets
exercised by the unit test:
// Public, generic but not testable interface:
fn find_matches(
reader: impl std::io::Read,
writer: impl std::io::Write,
pattern: &str,
fname: &std::path::Display,
) -> Result<(), Error> {
let mut buf_reader = BufReader::new(reader);
let mut buf_writer = BufWriter::new(writer);
find_matches_inner(&mut buf_reader, &mut buf_writer, pattern, fname)
}
// Testable implementation:
fn find_matches_inner(
reader: &mut BufReader<impl std::io::Read>,
writer: &mut BufWriter<impl std::io::Write>,
pattern: &str,
fname: &std::path::Display,
) -> Result<(), Error> {
let mut line = String::new();
let mut idx = 1;
while reader
.read_line(&mut line)
.context(format!("While reading {} at line {}", fname, idx))?
!= 0
{
if line.contains(pattern) {
write!(writer, "{}:{}: {}", fname, idx, line)?;
}
line.clear();
idx += 1;
}
Ok(())
}
#[test]
fn test_find_matches() {
let input = "Foobar".as_bytes();
let result = Vec::new();
let pattern = "Foo";
let fname = std::path::Path::new("./irrelevant");
let mut buf_reader = BufReader::new(input);
let mut buf_writer = BufWriter::new(result);
match find_matches_inner(&mut buf_reader, &mut buf_writer, pattern, &fname.display()) {
Ok(_) => {},
Err(e) => panic!("Unexpected error return: {}", e),
}
let result_as_str = match String::from_utf8(buf_writer.buffer().to_vec()) {
Ok(result_) => result_,
Err(_) => panic!("Could not parse."),
};
assert_eq!(result_as_str, "./irrelevant:1: Foobar");
}
This works, but I'm not comfortable with the solution because some errors that could happen to the clients, during
the buffer creation would not be testable, and it's quite a lot of hammering to make the function testable.
So, how could I test find_matches
by comparing writer
against an expected value, without the cumbersome
separation into two functions?