How to test a function, when ownership is transferred to it

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?

If T is Read, &mut T also is Read. Same applies for the Write. So you can pass references to that function.

1 Like

The problem is that the BufWriter needs to take ownership of the value, a borrow won't do, AFAIK. :-/

They can happily takes ownership of the references. Basically your function takes T: std::io::Read, and this T can be something like &mut std::io::Cursor<Vec<u8>>

In the context of generics, a type variable T means any type (meeting the prescribed trait bounds, if any). It doesn't mean "a non-reference type".

If you write the code below, it will happily compile (playground):

fn identity<T>(x: T) -> T {
    x
}

fn main() {
    let value = 42;
    let same_value = identity(value);

    let reference = &value;
    let same_reference = identity(reference);
}

In the second case, the compiler simply substitutes T = &i32.

I recommend reading this article on common misconceptions.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.